refactor unicsv & xcsv date time handling (#1114)
authortsteven4 <13596209+tsteven4@users.noreply.github.com>
Tue, 16 May 2023 21:44:47 +0000 (15:44 -0600)
committerGitHub <noreply@github.com>
Tue, 16 May 2023 21:44:47 +0000 (15:44 -0600)
* refactor unicsv date time handling.

* restrict utc option range to match Qt offsetfromutc

* move xcsv from C-style legacy time to Qt.

* fill in lower order date/time fields when parsing.

* don't return date/time if we don't have one!

* enhance xcsv date/time testing, fix bug.

* csv format date time adjustements.

use QDateTime::fromString to parse iso date times.
return invalid QTime from addhms if parsing fails entirely.

* datetime display fixes.

For the xcsv writer:
avoid priting date/time fields when there isn't a valid date/time.
when printing with am/pm use times from a 12 hour clock.
add support for printing dotnet time.
use QDateTime::toString to print ISO time.
For the xcsv and unicsv readers:
use QDateTime::fromString instead of xml_parse_time when reading
ISO datetimes.  This avoids xml_parse_times intentional odd behavior
of treating non-timezoned times as UTC.

add a test of the xcsv writer time related fields.  This only runs
in America/Denver time zone.  As setting the time zone is system
dependent the test only runs if tzselect is available.  If so it
assumes America/Denver is available.

correct documentation to give a sensible format for xcsv field GMT_TIME.

* add reference files for new test.

* silence xcsv reader conversion warnings on empty strings.

* warn on parse errors reading excel time.

* xcsv date/time fixes.

fix addhms to account for 12/24 hour clock.
don't print invalid datetimes with iso_time, iso_time_ms.

add testcases to exercise all the xcsv reader date/time flavors.

* clarify HMSL, HMSG wrt 12/24 hour clocks.

* add missing reference file.

* enhance xcsv for HMS[L|G] before or after [LOCAL|GMT]_TIME

* restore YYYYYMMDD to use UTC.

This has been broken for some time.  Mail from 2012 indicates the intent
was UTC (https://sourceforge.net/p/gpsbabel/mailman/message/29544538/)

* unicsv review catches

don't ripple into trouble with msec rounding.
pass outputs that may or may not be written as references.

* retire xcsv fields HMSG_TIME, HMSL_TIME.

This is potentially a user visible change.  It could require
users to rewrite any style files they have created that use
these fields.

These are replaced by repeated use of GMT_TIME and LOCAL_TIME.
This eliminates our pain over 12/24 hour clock issues. strptime/
strftime (as well as QTime) have distinct conversion specifiers
for hours with 12/24 hour clocks.  Our support for HMSG_TIME,
HMSL_TIME used integer conversion specifiers for hours, minutes
and seconds.  This made it difficult to decide if a 12 or 24 hour
clock should be used, and made it impossible to have reduced
precision values with 12 hour clock using an AM/PM designation.
We also always printed AM/PM designations.

* fix whitespace in serialization reference file

31 files changed:
defs.h
reference/datetime.xcsv [new file with mode: 0644]
reference/datetime_read.xcsv [new file with mode: 0644]
reference/datetime~xcsv.xcsv [new file with mode: 0644]
reference/format3.txt
reference/help.txt
reference/localgmttime.csv [new file with mode: 0644]
reference/localgmttime.xcsv [new file with mode: 0644]
reference/realtime.csv
reference/unicsv-local-utc.gpx [new file with mode: 0644]
reference/unicsv-local-utc1.gpx [new file with mode: 0644]
reference/unicsv-local.csv [new file with mode: 0644]
reference/unicsv-local.gpx [new file with mode: 0644]
reference/unicsv-local~csv.csv [new file with mode: 0644]
reference/unicsv-local~gpx.csv [new file with mode: 0644]
reference/unicsv-utc.csv [new file with mode: 0644]
reference/unicsv-utc.gpx [new file with mode: 0644]
style/garmin_g1000.style
style/iblue747.style
style/iblue757.style
style/land_air_sea.style
testo.d/track.test
testo.d/unicsv_local.test [new file with mode: 0644]
testo.d/xcsv.test
unicsv.cc
unicsv.h
util.cc
xcsv.cc
xcsv.h
xmldoc/chapters/styles.xml
xmldoc/formats/options/unicsv-utc.xml

diff --git a/defs.h b/defs.h
index 3d217d55935124fb655736a4622b4aed6a3af737..b4b988993faf9c012422f96222c905c0bdfe552b 100644 (file)
--- a/defs.h
+++ b/defs.h
@@ -27,6 +27,9 @@
 #include <optional>                  // for optional
 #include <utility>                   // for move
 
+#include <QByteArray>                // for QByteArray
+#include <QDate>                     // for QDate
+#include <QTime>                     // for QTime
 #include <QDateTime>                 // for QDateTime
 #include <QDebug>                    // for QDebug
 #include <QList>                     // for QList, QList<>::const_iterator, QList<>::const_reverse_iterator, QList<>::count, QList<>::reverse_iterator
@@ -1022,11 +1025,11 @@ inline int case_ignore_strncmp(const QString& s1, const QString& s2, int n)
 [[gnu::format(printf, 2, 0)]] int xvasprintf(char** strp, const char* fmt, va_list ap);
 char* strupper(char* src);
 char* strlower(char* src);
-time_t mklocaltime(std::tm* time);
-time_t mkgmtime(std::tm* time);
+QDateTime make_datetime(QDate date, QTime time, bool is_localtime, bool force_utc, int utc_offset);
 bool gpsbabel_testmode();
 gpsbabel::DateTime current_time();
 QDateTime dotnet_time_to_qdatetime(long long dotnet);
+long long qdatetime_to_dotnet_time(const QDateTime& dt);
 QString strip_html(const QString& utfstring);
 QString strip_nastyhtml(const QString& in);
 QString convert_human_date_format(const char* human_datef);    /* "MM,YYYY,DD" -> "%m,%Y,%d" */
diff --git a/reference/datetime.xcsv b/reference/datetime.xcsv
new file mode 100644 (file)
index 0000000..8c85138
--- /dev/null
@@ -0,0 +1,7 @@
+lat,lon,iso datetime
+40.000000,-105.070000,1970-01-03T00:04:05-07:00
+40.050000,-105.120000,1970-01-03T05:04:05-07:00
+40.120000,-105.190000,1970-01-04T12:04:05-07:00
+40.170000,-105.000000,1970-01-04T17:04:05-07:00
+40.040000,-105.110000,2069-12-31T04:05:06-07:00
+40.900000,-105.900000,
diff --git a/reference/datetime_read.xcsv b/reference/datetime_read.xcsv
new file mode 100644 (file)
index 0000000..44c0e5a
--- /dev/null
@@ -0,0 +1,7 @@
+LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME
+40.000000,-105.070000,1970-01-03T07:04:05Z
+40.050000,-105.120000,1970-01-03T12:04:05Z
+40.120000,-105.190000,1970-01-04T19:04:05Z
+40.170000,-105.000000,1970-01-05T00:04:05Z
+40.040000,-105.110000,2069-12-31T11:05:06Z
+40.900000,-105.900000,
diff --git a/reference/datetime~xcsv.xcsv b/reference/datetime~xcsv.xcsv
new file mode 100644 (file)
index 0000000..8d4df0f
--- /dev/null
@@ -0,0 +1,7 @@
+LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME
+40.000000,-105.070000,2.557129450231e+04,07:04:05 AM,1970-01-03 07:04:05,07:04:05,12:04:05 AM,1970-01-03 00:04:05,00:04:05,1970-01-03T07:04:05Z,1970-01-03T07:04:05Z,621357950450000000,198245,198245000,19700103
+40.050000,-105.120000,2.557150283565e+04,12:04:05 PM,1970-01-03 12:04:05,12:04:05,05:04:05 AM,1970-01-03 05:04:05,05:04:05,1970-01-03T12:04:05Z,1970-01-03T12:04:05Z,621358130450000000,216245,216245000,19700103
+40.120000,-105.190000,2.557279450231e+04,07:04:05 PM,1970-01-04 19:04:05,19:04:05,12:04:05 PM,1970-01-04 12:04:05,12:04:05,1970-01-04T19:04:05Z,1970-01-04T19:04:05Z,621359246450000000,327845,327845000,19700104
+40.170000,-105.000000,2.557300283565e+04,12:04:05 AM,1970-01-05 00:04:05,00:04:05,05:04:05 PM,1970-01-04 17:04:05,17:04:05,1970-01-05T00:04:05Z,1970-01-05T00:04:05Z,621359426450000000,345845,345845000,19700105
+40.040000,-105.110000,6.209346187500e+04,11:05:06 AM,2069-12-31 11:05:06,11:05:06,04:05:06 AM,2069-12-31 04:05:06,04:05:06,2069-12-31T11:05:06Z,2069-12-31T11:05:06Z,652913103060000000,3155713506,3155713506000,20691231
+40.900000,-105.900000,,,,,,,,,,,,,
index cbfa55b61a5691ed43d3948e734935447441d3d7..8d9e11974e6ecd42951ee521b0ef82ee6039b175 100644 (file)
@@ -16,6 +16,8 @@ option        xcsv    prefer_shortnames       Use shortname instead of description    boolean                         ht
 
 option xcsv    datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_xcsv.html#fmt_xcsv_o_datum
 
+option xcsv    utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_xcsv.html#fmt_xcsv_o_utc
+
 internal       rw----  tabsep          All database fields on one tab-separated line   xcsv
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_tabsep.html
 option tabsep  snlen   Max synthesized shortname length        integer         1               https://www.gpsbabel.org/WEB_DOC_DIR/fmt_tabsep.html#fmt_tabsep_o_snlen
@@ -32,6 +34,8 @@ option        tabsep  prefer_shortnames       Use shortname instead of description    boolean
 
 option tabsep  datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_tabsep.html#fmt_tabsep_o_datum
 
+option tabsep  utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_tabsep.html#fmt_tabsep_o_utc
+
 file   r-r---  v900            Columbus/Visiontac V900 files (.csv)    v900
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_v900.html
 file   rw----  csv             Comma separated values  xcsv
@@ -50,6 +54,8 @@ option        csv     prefer_shortnames       Use shortname instead of description    boolean                         htt
 
 option csv     datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_csv.html#fmt_csv_o_datum
 
+option csv     utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_csv.html#fmt_csv_o_utc
+
 internal       rw----  custom          Custom "Everything" Style       xcsv
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_custom.html
 option custom  snlen   Max synthesized shortname length        integer         1               https://www.gpsbabel.org/WEB_DOC_DIR/fmt_custom.html#fmt_custom_o_snlen
@@ -66,6 +72,8 @@ option        custom  prefer_shortnames       Use shortname instead of description    boolean
 
 option custom  datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_custom.html#fmt_custom_o_datum
 
+option custom  utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_custom.html#fmt_custom_o_utc
+
 file   --rw--  iblue747        csv     Data Logger iBlue747 csv        xcsv
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_iblue747.html
 option iblue747        snlen   Max synthesized shortname length        integer         1               https://www.gpsbabel.org/WEB_DOC_DIR/fmt_iblue747.html#fmt_iblue747_o_snlen
@@ -82,6 +90,8 @@ option        iblue747        prefer_shortnames       Use shortname instead of description    boolean
 
 option iblue747        datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_iblue747.html#fmt_iblue747_o_datum
 
+option iblue747        utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_iblue747.html#fmt_iblue747_o_utc
+
 file   --rw--  iblue757        csv     Data Logger iBlue757 csv        xcsv
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_iblue757.html
 option iblue757        snlen   Max synthesized shortname length        integer         1               https://www.gpsbabel.org/WEB_DOC_DIR/fmt_iblue757.html#fmt_iblue757_o_snlen
@@ -98,6 +108,8 @@ option       iblue757        prefer_shortnames       Use shortname instead of description    boolean
 
 option iblue757        datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_iblue757.html#fmt_iblue757_o_datum
 
+option iblue757        utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_iblue757.html#fmt_iblue757_o_utc
+
 file   rw----  exif    jpg     Embedded Exif-GPS data (.jpg)   exif
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_exif.html
 option exif    filename        Set waypoint name to source filename    boolean Y                       https://www.gpsbabel.org/WEB_DOC_DIR/fmt_exif.html#fmt_exif_o_filename
@@ -142,6 +154,8 @@ option      garmin301       prefer_shortnames       Use shortname instead of description    boolean
 
 option garmin301       datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_garmin301.html#fmt_garmin301_o_datum
 
+option garmin301       utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_garmin301.html#fmt_garmin301_o_utc
+
 file   --rw--  garmin_g1000    csv     Garmin G1000 datalog input filter file  xcsv
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_garmin_g1000.html
 option garmin_g1000    snlen   Max synthesized shortname length        integer         1               https://www.gpsbabel.org/WEB_DOC_DIR/fmt_garmin_g1000.html#fmt_garmin_g1000_o_snlen
@@ -158,6 +172,8 @@ option      garmin_g1000    prefer_shortnames       Use shortname instead of description    boole
 
 option garmin_g1000    datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_garmin_g1000.html#fmt_garmin_g1000_o_datum
 
+option garmin_g1000    utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_garmin_g1000.html#fmt_garmin_g1000_o_utc
+
 file   rwrwrw  gdb     gdb     Garmin MapSource - gdb  gdb
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_gdb.html
 option gdb     cat     Default category on output (1..16)      integer         1       16      https://www.gpsbabel.org/WEB_DOC_DIR/fmt_gdb.html#fmt_gdb_o_cat
@@ -206,6 +222,8 @@ option      garmin_poi      prefer_shortnames       Use shortname instead of description    boolean
 
 option garmin_poi      datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_garmin_poi.html#fmt_garmin_poi_o_datum
 
+option garmin_poi      utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_garmin_poi.html#fmt_garmin_poi_o_utc
+
 file   rw----  garmin_gpi      gpi     Garmin Points of Interest (.gpi)        garmin_gpi
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_garmin_gpi.html
 option garmin_gpi      alerts  Enable alerts on speed or proximity distance    boolean                         https://www.gpsbabel.org/WEB_DOC_DIR/fmt_garmin_gpi.html#fmt_garmin_gpi_o_alerts
@@ -358,6 +376,8 @@ option      land_air_sea    prefer_shortnames       Use shortname instead of description    boole
 
 option land_air_sea    datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_land_air_sea.html#fmt_land_air_sea_o_datum
 
+option land_air_sea    utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_land_air_sea.html#fmt_land_air_sea_o_utc
+
 file   rwrwrw  gtm     gtm     GPS TrackMaker  gtm
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_gtm.html
 file   rw----  arc     txt     GPSBabel arc filter file        xcsv
@@ -376,6 +396,8 @@ option      arc     prefer_shortnames       Use shortname instead of description    boolean                         htt
 
 option arc     datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_arc.html#fmt_arc_o_datum
 
+option arc     utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_arc.html#fmt_arc_o_utc
+
 file   rw----  gpsdrive                GpsDrive Format xcsv
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_gpsdrive.html
 option gpsdrive        snlen   Max synthesized shortname length        integer         1               https://www.gpsbabel.org/WEB_DOC_DIR/fmt_gpsdrive.html#fmt_gpsdrive_o_snlen
@@ -392,6 +414,8 @@ option      gpsdrive        prefer_shortnames       Use shortname instead of description    boolean
 
 option gpsdrive        datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_gpsdrive.html#fmt_gpsdrive_o_datum
 
+option gpsdrive        utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_gpsdrive.html#fmt_gpsdrive_o_utc
+
 file   rw----  gpsdrivetrack           GpsDrive Format for Tracks      xcsv
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_gpsdrivetrack.html
 option gpsdrivetrack   snlen   Max synthesized shortname length        integer         1               https://www.gpsbabel.org/WEB_DOC_DIR/fmt_gpsdrivetrack.html#fmt_gpsdrivetrack_o_snlen
@@ -408,6 +432,8 @@ option      gpsdrivetrack   prefer_shortnames       Use shortname instead of description    bool
 
 option gpsdrivetrack   datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_gpsdrivetrack.html#fmt_gpsdrivetrack_o_datum
 
+option gpsdrivetrack   utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_gpsdrivetrack.html#fmt_gpsdrivetrack_o_utc
+
 file   rwrwrw  gpx     gpx     GPX XML gpx
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_gpx.html
 option gpx     snlen   Length of generated shortnames  integer 32      1               https://www.gpsbabel.org/WEB_DOC_DIR/fmt_gpx.html#fmt_gpx_o_snlen
@@ -686,6 +712,8 @@ option      openoffice      prefer_shortnames       Use shortname instead of description    boolean
 
 option openoffice      datum   GPS datum (def. WGS 84) string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_openoffice.html#fmt_openoffice_o_datum
 
+option openoffice      utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_openoffice.html#fmt_openoffice_o_utc
+
 file   -w----  text    txt     Textual Output  text
        https://www.gpsbabel.org/WEB_DOC_DIR/fmt_text.html
 option text    nosep   Suppress separator lines between waypoints      boolean                         https://www.gpsbabel.org/WEB_DOC_DIR/fmt_text.html#fmt_text_o_nosep
@@ -706,7 +734,7 @@ option      unicsv  datum   GPS datum (def. WGS 84) string  WGS 84                  https://www.gpsbabel
 
 option unicsv  grid    Write position using this grid. string                          https://www.gpsbabel.org/WEB_DOC_DIR/fmt_unicsv.html#fmt_unicsv_o_grid
 
-option unicsv  utc     Write timestamps with offset x to UTC time      integer         -23     +23     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_unicsv.html#fmt_unicsv_o_utc
+option unicsv  utc     Write timestamps with offset x to UTC time      integer         -14     +14     https://www.gpsbabel.org/WEB_DOC_DIR/fmt_unicsv.html#fmt_unicsv_o_utc
 
 option unicsv  format  Write name(s) of format(s) from input session(s)        boolean                         https://www.gpsbabel.org/WEB_DOC_DIR/fmt_unicsv.html#fmt_unicsv_o_format
 
index abdfc567203d1a9c18eba053c503f9257d498a45..346a8a791f3da02dab457cb2cde5061fcc08ea7e 100644 (file)
@@ -41,6 +41,7 @@ File Types (-i and -o options):
          urlbase               Basename prepended to URL on output
          prefer_shortnames     (0/1) Use shortname instead of description
          datum                 GPS datum (def. WGS 84)
+         utc                   Write timestamps with offset x to UTC time
        iblue747              Data Logger iBlue747 csv
          snlen                 Max synthesized shortname length
          snwhite               (0/1) Allow whitespace synth. shortnames
@@ -49,6 +50,7 @@ File Types (-i and -o options):
          urlbase               Basename prepended to URL on output
          prefer_shortnames     (0/1) Use shortname instead of description
          datum                 GPS datum (def. WGS 84)
+         utc                   Write timestamps with offset x to UTC time
        iblue757              Data Logger iBlue757 csv
          snlen                 Max synthesized shortname length
          snwhite               (0/1) Allow whitespace synth. shortnames
@@ -57,6 +59,7 @@ File Types (-i and -o options):
          urlbase               Basename prepended to URL on output
          prefer_shortnames     (0/1) Use shortname instead of description
          datum                 GPS datum (def. WGS 84)
+         utc                   Write timestamps with offset x to UTC time
        exif                  Embedded Exif-GPS data (.jpg)
          filename              (0/1) Set waypoint name to source filename
          frame                 Time-frame (in seconds)
@@ -79,6 +82,7 @@ File Types (-i and -o options):
          urlbase               Basename prepended to URL on output
          prefer_shortnames     (0/1) Use shortname instead of description
          datum                 GPS datum (def. WGS 84)
+         utc                   Write timestamps with offset x to UTC time
        garmin_g1000          Garmin G1000 datalog input filter file
          snlen                 Max synthesized shortname length
          snwhite               (0/1) Allow whitespace synth. shortnames
@@ -87,6 +91,7 @@ File Types (-i and -o options):
          urlbase               Basename prepended to URL on output
          prefer_shortnames     (0/1) Use shortname instead of description
          datum                 GPS datum (def. WGS 84)
+         utc                   Write timestamps with offset x to UTC time
        gdb                   Garmin MapSource - gdb
          cat                   Default category on output (1..16)
          bitscategory          Bitmap of categories
@@ -111,6 +116,7 @@ File Types (-i and -o options):
          urlbase               Basename prepended to URL on output
          prefer_shortnames     (0/1) Use shortname instead of description
          datum                 GPS datum (def. WGS 84)
+         utc                   Write timestamps with offset x to UTC time
        garmin_gpi            Garmin Points of Interest (.gpi)
          alerts                (0/1) Enable alerts on speed or proximity distance
          bitmap                Use specified bitmap on output
@@ -181,6 +187,7 @@ File Types (-i and -o options):
          urlbase               Basename prepended to URL on output
          prefer_shortnames     (0/1) Use shortname instead of description
          datum                 GPS datum (def. WGS 84)
+         utc                   Write timestamps with offset x to UTC time
        gtm                   GPS TrackMaker
        arc                   GPSBabel arc filter file
          snlen                 Max synthesized shortname length
@@ -190,6 +197,7 @@ File Types (-i and -o options):
          urlbase               Basename prepended to URL on output
          prefer_shortnames     (0/1) Use shortname instead of description
          datum                 GPS datum (def. WGS 84)
+         utc                   Write timestamps with offset x to UTC time
        gpsdrive              GpsDrive Format
          snlen                 Max synthesized shortname length
          snwhite               (0/1) Allow whitespace synth. shortnames
@@ -198,6 +206,7 @@ File Types (-i and -o options):
          urlbase               Basename prepended to URL on output
          prefer_shortnames     (0/1) Use shortname instead of description
          datum                 GPS datum (def. WGS 84)
+         utc                   Write timestamps with offset x to UTC time
        gpsdrivetrack         GpsDrive Format for Tracks
          snlen                 Max synthesized shortname length
          snwhite               (0/1) Allow whitespace synth. shortnames
@@ -206,6 +215,7 @@ File Types (-i and -o options):
          urlbase               Basename prepended to URL on output
          prefer_shortnames     (0/1) Use shortname instead of description
          datum                 GPS datum (def. WGS 84)
+         utc                   Write timestamps with offset x to UTC time
        gpx                   GPX XML
          snlen                 Length of generated shortnames
          suppresswhite         (0/1) No whitespace in generated shortnames
@@ -341,6 +351,7 @@ File Types (-i and -o options):
          urlbase               Basename prepended to URL on output
          prefer_shortnames     (0/1) Use shortname instead of description
          datum                 GPS datum (def. WGS 84)
+         utc                   Write timestamps with offset x to UTC time
        text                  Textual Output
          nosep                 (0/1) Suppress separator lines between waypoints
          encrypt               (0/1) Encrypt hints using ROT13
diff --git a/reference/localgmttime.csv b/reference/localgmttime.csv
new file mode 100644 (file)
index 0000000..9f8e0e3
--- /dev/null
@@ -0,0 +1,3 @@
+No,Latitude,Longitude,Name,Date,Time\r
+1,40.000000,-105.000000,"WPT001",1970/01/03,03:04:05\r
+2,40.100000,-105.100000,"WPT002",2069/12/31,04:05:06\r
diff --git a/reference/localgmttime.xcsv b/reference/localgmttime.xcsv
new file mode 100644 (file)
index 0000000..5ed0b96
--- /dev/null
@@ -0,0 +1,2 @@
+40.000000,-105.000000,1970-01-03,03:04:05,1970-01-03T03:04:05-07:00
+40.100000,-105.100000,2069-12-31,04:05:06,2069-12-31T04:05:06-07:00
index 48ee8caf3ad1cc5e005897944d6239173e2f9485..7c83ad03a845938f07036ddda22523259746b038 100644 (file)
@@ -1,4 +1,4 @@
--28.606309,41.491196,85.918,Wpt_RD,1970-01-01T00:00:00Z
+-28.606309,41.491196,85.918,Wpt_RD,
 -28.605513,41.492136,0.027,Wpt_ahVv,1970-01-01T00:00:01.189Z
 -28.605013,41.492918,78.576,Wpt_ElFRt5,1970-01-01T00:00:02.317Z
 -28.604849,41.492946,,Wpt_Stg4W,1970-01-01T00:00:04.160Z
diff --git a/reference/unicsv-local-utc.gpx b/reference/unicsv-local-utc.gpx
new file mode 100644 (file)
index 0000000..76fbfb0
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gpx version="1.0" creator="GPSBabel - https://www.gpsbabel.org" xmlns="http://www.topografix.com/GPX/1/0">
+  <time>1970-01-01T00:00:00Z</time>
+  <bounds minlat="36.000000000" minlon="-87.000000000" maxlat="36.000000000" maxlon="-87.000000000"/>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-01-10T23:00:00Z</time>
+    <name>WPT001</name>
+    <cmt>WPT001</cmt>
+    <desc>WPT001</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-07-10T23:00:00Z</time>
+    <name>WPT001</name>
+    <cmt>WPT001</cmt>
+    <desc>WPT001</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>1970-01-01T23:00:00Z</time>
+    <name>WPT002</name>
+    <cmt>WPT002</cmt>
+    <desc>WPT002</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-01-10T00:00:00Z</time>
+    <name>WPT002</name>
+    <cmt>WPT002</cmt>
+    <desc>WPT002</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-07-10T00:00:00Z</time>
+    <name>WPT002</name>
+    <cmt>WPT002</cmt>
+    <desc>WPT002</desc>
+  </wpt>
+</gpx>
diff --git a/reference/unicsv-local-utc1.gpx b/reference/unicsv-local-utc1.gpx
new file mode 100644 (file)
index 0000000..cd38eb9
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gpx version="1.0" creator="GPSBabel - https://www.gpsbabel.org" xmlns="http://www.topografix.com/GPX/1/0">
+  <time>1970-01-01T00:00:00Z</time>
+  <bounds minlat="36.000000000" minlon="-87.000000000" maxlat="36.000000000" maxlon="-87.000000000"/>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-01-10T22:00:00Z</time>
+    <name>WPT001</name>
+    <cmt>WPT001</cmt>
+    <desc>WPT001</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-07-10T22:00:00Z</time>
+    <name>WPT001</name>
+    <cmt>WPT001</cmt>
+    <desc>WPT001</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>1970-01-01T22:00:00Z</time>
+    <name>WPT002</name>
+    <cmt>WPT002</cmt>
+    <desc>WPT002</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-01-09T23:00:00Z</time>
+    <name>WPT002</name>
+    <cmt>WPT002</cmt>
+    <desc>WPT002</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-07-09T23:00:00Z</time>
+    <name>WPT002</name>
+    <cmt>WPT002</cmt>
+    <desc>WPT002</desc>
+  </wpt>
+</gpx>
diff --git a/reference/unicsv-local.csv b/reference/unicsv-local.csv
new file mode 100644 (file)
index 0000000..a91fe72
--- /dev/null
@@ -0,0 +1,6 @@
+No,Latitude,Longitude,Name,Date,Time\r
+1,36.000000,-87.000000,"WPT001",2022/01/10,23:00:00\r
+2,36.000000,-87.000000,"WPT001",2022/07/10,23:00:00\r
+3,36.000000,-87.000000,"WPT002",,23:00:00\r
+4,36.000000,-87.000000,"WPT002",2022/01/10,\r
+5,36.000000,-87.000000,"WPT002",2022/07/10,\r
diff --git a/reference/unicsv-local.gpx b/reference/unicsv-local.gpx
new file mode 100644 (file)
index 0000000..fe131a1
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gpx version="1.0" creator="GPSBabel - https://www.gpsbabel.org" xmlns="http://www.topografix.com/GPX/1/0">
+  <time>1970-01-01T00:00:00Z</time>
+  <bounds minlat="36.000000000" minlon="-87.000000000" maxlat="36.000000000" maxlon="-87.000000000"/>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-01-11T07:00:00Z</time>
+    <name>WPT001</name>
+    <cmt>WPT001</cmt>
+    <desc>WPT001</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-07-11T06:00:00Z</time>
+    <name>WPT001</name>
+    <cmt>WPT001</cmt>
+    <desc>WPT001</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>1970-01-02T07:00:00Z</time>
+    <name>WPT002</name>
+    <cmt>WPT002</cmt>
+    <desc>WPT002</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-01-10T08:00:00Z</time>
+    <name>WPT002</name>
+    <cmt>WPT002</cmt>
+    <desc>WPT002</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-07-10T07:00:00Z</time>
+    <name>WPT002</name>
+    <cmt>WPT002</cmt>
+    <desc>WPT002</desc>
+  </wpt>
+</gpx>
diff --git a/reference/unicsv-local~csv.csv b/reference/unicsv-local~csv.csv
new file mode 100644 (file)
index 0000000..f755847
--- /dev/null
@@ -0,0 +1,6 @@
+No,Latitude,Longitude,Name,Date,Time\r
+1,36.000000,-87.000000,"WPT001",2022/01/10,23:00:00\r
+2,36.000000,-87.000000,"WPT001",2022/07/10,23:00:00\r
+3,36.000000,-87.000000,"WPT002",,23:00:00\r
+4,36.000000,-87.000000,"WPT002",2022/01/10,00:00:00\r
+5,36.000000,-87.000000,"WPT002",2022/07/10,00:00:00\r
diff --git a/reference/unicsv-local~gpx.csv b/reference/unicsv-local~gpx.csv
new file mode 100644 (file)
index 0000000..f755847
--- /dev/null
@@ -0,0 +1,6 @@
+No,Latitude,Longitude,Name,Date,Time\r
+1,36.000000,-87.000000,"WPT001",2022/01/10,23:00:00\r
+2,36.000000,-87.000000,"WPT001",2022/07/10,23:00:00\r
+3,36.000000,-87.000000,"WPT002",,23:00:00\r
+4,36.000000,-87.000000,"WPT002",2022/01/10,00:00:00\r
+5,36.000000,-87.000000,"WPT002",2022/07/10,00:00:00\r
diff --git a/reference/unicsv-utc.csv b/reference/unicsv-utc.csv
new file mode 100644 (file)
index 0000000..5b027af
--- /dev/null
@@ -0,0 +1,6 @@
+No,Latitude,Longitude,Name,utc_d,utc_t\r
+1,36.000000,-87.000000,"WPT001",2022/01/10,23:00:00\r
+2,36.000000,-87.000000,"WPT001",2022/07/10,23:00:00\r
+3,36.000000,-87.000000,"WPT002",,23:00:00\r
+4,36.000000,-87.000000,"WPT002",2022/01/10,\r
+5,36.000000,-87.000000,"WPT002",2022/07/10,\r
diff --git a/reference/unicsv-utc.gpx b/reference/unicsv-utc.gpx
new file mode 100644 (file)
index 0000000..76fbfb0
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gpx version="1.0" creator="GPSBabel - https://www.gpsbabel.org" xmlns="http://www.topografix.com/GPX/1/0">
+  <time>1970-01-01T00:00:00Z</time>
+  <bounds minlat="36.000000000" minlon="-87.000000000" maxlat="36.000000000" maxlon="-87.000000000"/>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-01-10T23:00:00Z</time>
+    <name>WPT001</name>
+    <cmt>WPT001</cmt>
+    <desc>WPT001</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-07-10T23:00:00Z</time>
+    <name>WPT001</name>
+    <cmt>WPT001</cmt>
+    <desc>WPT001</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>1970-01-01T23:00:00Z</time>
+    <name>WPT002</name>
+    <cmt>WPT002</cmt>
+    <desc>WPT002</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-01-10T00:00:00Z</time>
+    <name>WPT002</name>
+    <cmt>WPT002</cmt>
+    <desc>WPT002</desc>
+  </wpt>
+  <wpt lat="36.000000000" lon="-87.000000000">
+    <time>2022-07-10T00:00:00Z</time>
+    <name>WPT002</name>
+    <cmt>WPT002</cmt>
+    <desc>WPT002</desc>
+  </wpt>
+</gpx>
index 1855563cddcbf6494a09dcaf02f6b4bc275ee2c3..593a1162f6bd1e3345a82d81a6a4b2f01a177e03 100644 (file)
@@ -29,7 +29,7 @@ PROLOGUE Lcl Date, Lcl Time, UTCOfst, AtvWpt,     Latitude,    Longitude,    Alt
 # INDIVIDUAL DATA FIELDS:
 #
 IFIELD GMT_TIME,"","%Y-%m-%d"          #Lcl Date,
-IFIELD HMSG_TIME,"","%d:%d:%d %s"      #Lcl Time,
+IFIELD GMT_TIME,"","%H:%M:%S"          #Lcl Time,
 IFIELD IGNORE,"","%s"                                  #UTCOfst,
 IFIELD IGNORE,"","%s"                                  #AtvWpt,
 IFIELD LAT_DECIMAL, "", "%f"           #Latitude,
index 2b0b75978cd75a65c388f950b38f8e72a1c7df84..838faa211203c6437137ef41a66ca6274699dcd8 100644 (file)
@@ -23,7 +23,7 @@ PROLOGUE INDEX,RCR,DATE,TIME,VALID,LATITUDE,N/S,LONGITUDE,E/W,HEIGHT,SPEED,HEADI
 IFIELD INDEX,"1","%d"            # INDEX
 IFIELD CONSTANT,"T","%s"        # RCR
 IFIELD GMT_TIME,"","%Y/%m/%d"        # DATE
-IFIELD HMSG_TIME,"","%02d:%02d:%02d"    # TIME
+IFIELD GMT_TIME,"","%H:%M:%S"    # TIME
 IFIELD GPS_FIX,"","%s"        # VALID #  No fix, SPS, DGPS, PPS
 IFIELD LAT_DECIMAL,"","%f"        # LATITUDE
 IFIELD LAT_DIR,"","%c"            # N/S
index 1e0395e66b9318228dbb5a508c9c320f7d7aac8e..db4dd274cdf6e52e8741253b98c840507f06edb8 100644 (file)
@@ -24,7 +24,7 @@ PROLOGUE INDEX,RCR,DATE,TIME,VALID,LATITUDE,N/S,LONGITUDE,E/W,HEIGHT,SPEED,HEADI
 IFIELD INDEX,"1","%d"            # INDEX
 IFIELD CONSTANT,"T","%s"        # RCR
 IFIELD GMT_TIME,"","%d/%m/%Y"        # DATE
-IFIELD HMSG_TIME,"","%02d:%02d:%02d"    # TIME
+IFIELD GMT_TIME,"","%H:%M:%S"    # TIME
 IFIELD GPS_FIX,"","%s"        # VALID #  No fix, SPS, DGPS, PPS
 IFIELD LAT_DECIMAL,"","%f"        # LATITUDE
 IFIELD LAT_DIR,"","%c"            # N/S
index 388b28bdfba86b994aac18f0e65249c2d5417d51..d14034e5c43a7fb02dd5d1cf5ca180bd761fe4a3 100644 (file)
@@ -16,7 +16,7 @@ RECORD_DELIMITER NEWLINE
 # Individual data fields in order of appearance
 
 IFIELD LOCAL_TIME,"","%m-%d-%Y"
-IFIELD HMSG_TIME,"","%d:%d:%d"
+IFIELD LOCAL_TIME,"","%H:%M:%S"
 IFIELD LAT_HUMAN_READABLE,"","%c %d°%d'%f\""
 IFIELD LON_HUMAN_READABLE,"","%c %d°%d'%f\""
 IFIELD PATH_SPEED_MPH,"","%.1fmph"
index adb9afbe18323790fcef5277774ae92cd22412ea..299247e56d2e29603879bcd94e6c1a9b741f28e9 100644 (file)
@@ -21,7 +21,7 @@ compare ${REFERENCE}/track/trackfilter3.gpx ${TMPDIR}/trackfilter3.gpx
 
 # Exercise the 'faketime' filter.  The middle of the three points has
 # time so we can exercise the 'forced' option, too.
-gpsbabel -t -i unicsv -f ${REFERENCE}/track/trackfilter_faketime.txt -x track,faketime=20100506060000+5 -o gpx -F ${TMPDIR}/ft.gpx 
+gpsbabel -t -i unicsv,utc -f ${REFERENCE}/track/trackfilter_faketime.txt -x track,faketime=20100506060000+5 -o gpx -F ${TMPDIR}/ft.gpx
 compare ${REFERENCE}/track/trackfilter_faketime.gpx ${TMPDIR}/ft.gpx
 gpsbabel -t -i unicsv -f ${REFERENCE}/track/trackfilter_faketime.txt -x track,faketime=f20100506060000+5 -o gpx -F ${TMPDIR}/ftf.gpx
 compare ${REFERENCE}/track/trackfilter_faketime_forced.gpx ${TMPDIR}/ftf.gpx
diff --git a/testo.d/unicsv_local.test b/testo.d/unicsv_local.test
new file mode 100644 (file)
index 0000000..8cc57aa
--- /dev/null
@@ -0,0 +1,43 @@
+if command -v tzselect >/dev/null 2>&1 ; then
+    export TZ='America/Los_Angeles'
+
+    echo "  including local timezone tests"
+    # test interpretation of local date and time,
+    # with and without optional utc override, with and without offsets.
+    # these gpx files erroneously show the invented 1970/01/01 date.
+    gpsbabel -i unicsv -f ${REFERENCE}/unicsv-local.csv -o gpx -F ${TMPDIR}/unicsv-local.gpx
+    compare ${REFERENCE}/unicsv-local.gpx ${TMPDIR}/unicsv-local.gpx
+    gpsbabel -i unicsv,utc -f ${REFERENCE}/unicsv-local.csv -o gpx -F ${TMPDIR}/unicsv-local-utc.gpx
+    compare ${REFERENCE}/unicsv-local-utc.gpx ${TMPDIR}/unicsv-local-utc.gpx
+    gpsbabel -i unicsv,utc=1 -f ${REFERENCE}/unicsv-local.csv -o gpx -F ${TMPDIR}/unicsv-local-utc1.gpx
+    compare ${REFERENCE}/unicsv-local-utc1.gpx ${TMPDIR}/unicsv-local-utc1.gpx
+
+    # test display of local date and time,
+    # with and without optional utc override, with and without offsets.
+    gpsbabel -i gpx -f ${REFERENCE}/unicsv-local.gpx -o unicsv -F ${TMPDIR}/unicsv-local~gpx.csv
+    compare ${REFERENCE}/unicsv-local~gpx.csv ${TMPDIR}/unicsv-local~gpx.csv
+    gpsbabel -i gpx -f ${REFERENCE}/unicsv-local-utc.gpx -o unicsv,utc -F ${TMPDIR}/unicsv-local-utc~gpx.csv
+    compare ${REFERENCE}/unicsv-local~gpx.csv ${TMPDIR}/unicsv-local-utc~gpx.csv
+    gpsbabel -i gpx -f ${REFERENCE}/unicsv-local-utc1.gpx -o unicsv,utc=1 -F ${TMPDIR}/unicsv-local-utc1~gpx.csv
+    compare ${REFERENCE}/unicsv-local~gpx.csv ${TMPDIR}/unicsv-local-utc1~gpx.csv
+
+    # test echo of local date and time,
+    # with and without optional utc override, with and without offsets.
+    gpsbabel -i unicsv -f ${REFERENCE}/unicsv-local.csv -o unicsv -F ${TMPDIR}/unicsv-local~csv.csv
+    compare ${REFERENCE}/unicsv-local~csv.csv ${TMPDIR}/unicsv-local~csv.csv
+    gpsbabel -i unicsv,utc -f ${REFERENCE}/unicsv-local.csv -o unicsv,utc -F ${TMPDIR}/unicsv-local-utc~csv.csv
+    compare ${REFERENCE}/unicsv-local~csv.csv ${TMPDIR}/unicsv-local-utc~csv.csv
+    gpsbabel -i unicsv,utc=1 -f ${REFERENCE}/unicsv-local.csv -o unicsv,utc=1 -F ${TMPDIR}/unicsv-local-utc1~csv.csv
+    compare ${REFERENCE}/unicsv-local~csv.csv ${TMPDIR}/unicsv-local-utc1~csv.csv
+
+    unset -v TZ
+fi
+
+# make sure utc_d, utc_t ignore utc option - it only overrides local times.
+gpsbabel -i unicsv -f ${REFERENCE}/unicsv-utc.csv -o gpx -F ${TMPDIR}/unicsv-utc.gpx
+compare ${REFERENCE}/unicsv-utc.gpx ${TMPDIR}/unicsv-utc.gpx
+gpsbabel -i unicsv,utc -f ${REFERENCE}/unicsv-utc.csv -o gpx -F ${TMPDIR}/unicsv-utc-utc.gpx
+compare ${REFERENCE}/unicsv-utc.gpx ${TMPDIR}/unicsv-utc-utc.gpx
+gpsbabel -i unicsv,utc=1 -f ${REFERENCE}/unicsv-utc.csv -o gpx -F ${TMPDIR}/unicsv-utc-utc1.gpx
+compare ${REFERENCE}/unicsv-utc.gpx ${TMPDIR}/unicsv-utc-utc1.gpx
+
index a356f63ebea33dfa8d323dc0e1e88ae99a091703..2a745ab8d3766090f6f779b927c13dbabdf277dd 100644 (file)
@@ -28,13 +28,13 @@ echo "IFIELD SHORTNAME, , %s" >> ${TMPDIR}/testo2.style
 echo "IFIELD ALT_METERS, -99999999.0, %.0f" >> ${TMPDIR}/testo2.style
 echo "IFIELD IGNORE, , %s"  >> ${TMPDIR}/testo2.style
 echo "IFIELD GMT_TIME, , %Y/%m/%d" >> ${TMPDIR}/testo2.style
-echo "IFIELD HMSG_TIME,  , %d:%d:%d" >> ${TMPDIR}/testo2.style
+echo "IFIELD GMT_TIME,  , %H:%M:%S" >> ${TMPDIR}/testo2.style
 rm -f ${TMPDIR}/grid-utm~xscv.gpx
 gpsbabel -i xcsv,style=${TMPDIR}/testo2.style -f ${REFERENCE}/grid-utm.csv -o gpx -F ${TMPDIR}/grid-utm~xscv.gpx
 compare ${REFERENCE}/grid-utm~xscv.gpx ${TMPDIR}/grid-utm~xscv.gpx
 
 # test TRACK_NAME, TRACK_NEW
-echo 'DESCRIPTION track style test 1' >>${TMPDIR}/track1.style
+echo 'DESCRIPTION track style test 1' >${TMPDIR}/track1.style
 echo 'EXTENSION csv' >>${TMPDIR}/track1.style
 echo 'FIELD_DELIMITER COMMA' >>${TMPDIR}/track1.style
 echo 'RECORD_DELIMITER NEWLINE' >>${TMPDIR}/track1.style
@@ -47,7 +47,7 @@ gpsbabel -i xcsv,style=${TMPDIR}/track1.style -f ${REFERENCE}/track/track1.csv -
 compare ${REFERENCE}/track/track1-2~csv.gpx ${TMPDIR}/track1~csv.gpx
 
 # flip TRACK_NAME, TRACK_NEW order
-echo 'DESCRIPTION track style test 2' >>${TMPDIR}/track2.style
+echo 'DESCRIPTION track style test 2' >${TMPDIR}/track2.style
 echo 'EXTENSION csv' >>${TMPDIR}/track2.style
 echo 'FIELD_DELIMITER COMMA' >>${TMPDIR}/track2.style
 echo 'RECORD_DELIMITER NEWLINE' >>${TMPDIR}/track2.style
@@ -60,7 +60,7 @@ gpsbabel -i xcsv,style=${TMPDIR}/track2.style -f ${REFERENCE}/track/track2.csv -
 compare ${REFERENCE}/track/track1-2~csv.gpx ${TMPDIR}/track2~csv.gpx
 
 # ROUTE_NAME
-echo 'DESCRIPTION route style test 1' >>${TMPDIR}/route1.style
+echo 'DESCRIPTION route style test 1' >${TMPDIR}/route1.style
 echo 'EXTENSION csv' >>${TMPDIR}/route1.style
 echo 'FIELD_DELIMITER COMMA' >>${TMPDIR}/route1.style
 echo 'RECORD_DELIMITER NEWLINE' >>${TMPDIR}/route1.style
@@ -72,7 +72,7 @@ gpsbabel -i xcsv,style=${TMPDIR}/route1.style -f ${REFERENCE}/route/route1.csv -
 compare ${REFERENCE}/route/route1~csv.gpx ${TMPDIR}/route1~csv.gpx
 
 # gmsd fields
-echo 'DESCRIPTION gmsd test' >> ${TMPDIR}/gmsd.style
+echo 'DESCRIPTION gmsd test' > ${TMPDIR}/gmsd.style
 echo 'EXTENSION csv' >> ${TMPDIR}/gmsd.style
 echo 'FIELD_DELIMITER TAB' >> ${TMPDIR}/gmsd.style
 echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/gmsd.style
@@ -92,3 +92,337 @@ gpsbabel -i unicsv -f ${REFERENCE}/gmsd.unicsv -o xcsv,style=${TMPDIR}/gmsd.styl
 compare ${REFERENCE}/gmsd.xcsv ${TMPDIR}/gmsd.xcsv
 gpsbabel -i xcsv,style=${TMPDIR}/gmsd.style -f ${REFERENCE}/gmsd.xcsv -o unicsv -F ${TMPDIR}/gmsd.unicsv
 compare ${REFERENCE}/gmsd.unicsv ${TMPDIR}/gmsd.unicsv
+
+if command -v tzselect >/dev/null 2>&1 ; then
+  export TZ='America/Denver'
+  echo "  including xcsv timezone conversion test"
+
+# xcsv writer time handling
+  echo 'FIELD_DELIMITER COMMA' > ${TMPDIR}/datetime.style
+  echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/datetime.style
+  echo 'PROLOGUE LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME' >> ${TMPDIR}/datetime.style
+  echo 'IFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime.style
+  echo 'IFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime.style
+  echo 'IFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD EXCEL_TIME, "", "%.12e"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD GMT_TIME, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD GMT_TIME, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD GMT_TIME, "", "%H:%M:%S"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD LOCAL_TIME, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD LOCAL_TIME, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD LOCAL_TIME, "", "%H:%M:%S"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD ISO_TIME_MS, "", "%s"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD NET_TIME, "", "%lld"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD TIMET_TIME, "", "%lld"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD TIMET_TIME_MS, "", "%lld"' >> ${TMPDIR}/datetime.style
+  echo 'OFIELD YYYYMMDD_TIME, "", "%ld"' >> ${TMPDIR}/datetime.style
+  gpsbabel -i xcsv,style=${TMPDIR}/datetime.style -f ${REFERENCE}/datetime.xcsv -o xcsv,style=${TMPDIR}/datetime.style -F ${TMPDIR}/datetime~xcsv.xcsv
+  compare ${REFERENCE}/datetime~xcsv.xcsv ${TMPDIR}/datetime~xcsv.xcsv
+
+# xcsv reader time handling
+
+  echo 'FIELD_DELIMITER COMMA' > ${TMPDIR}/datetime_excel.style
+  echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/datetime_excel.style
+  echo 'PROLOGUE LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME' >> ${TMPDIR}/datetime_excel.style
+  echo 'OFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_excel.style
+  echo 'OFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_excel.style
+  echo 'OFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD EXCEL_TIME, "", "%.12e"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_excel.style
+  echo 'IFIELD IGNORE, "", "%ld"' >> ${TMPDIR}/datetime_excel.style
+
+  gpsbabel -i xcsv,style=${TMPDIR}/datetime_excel.style -f ${REFERENCE}/datetime~xcsv.xcsv -o xcsv,style=${TMPDIR}/datetime_excel.style -F ${TMPDIR}/datetime_excel.xcsv
+  compare ${REFERENCE}/datetime_read.xcsv ${TMPDIR}/datetime_excel.xcsv
+
+  echo 'FIELD_DELIMITER COMMA' > ${TMPDIR}/datetime_gmt.style
+  echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/datetime_gmt.style
+  echo 'PROLOGUE LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME' >> ${TMPDIR}/datetime_gmt.style
+  echo 'OFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'OFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'OFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD IGNORE, "", "%.12e"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD GMT_TIME, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_gmt.style
+  echo 'IFIELD IGNORE, "", "%ld"' >> ${TMPDIR}/datetime_gmt.style
+
+  gpsbabel -i xcsv,style=${TMPDIR}/datetime_gmt.style -f ${REFERENCE}/datetime~xcsv.xcsv -o xcsv,style=${TMPDIR}/datetime_gmt.style -F ${TMPDIR}/datetime_gmt.xcsv
+  compare ${REFERENCE}/datetime_read.xcsv ${TMPDIR}/datetime_gmt.xcsv
+
+  echo 'FIELD_DELIMITER COMMA' > ${TMPDIR}/datetime_iso.style
+  echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/datetime_iso.style
+  echo 'PROLOGUE LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME' >> ${TMPDIR}/datetime_iso.style
+  echo 'OFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_iso.style
+  echo 'OFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_iso.style
+  echo 'OFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD IGNORE, "", "%.12e"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_iso.style
+  echo 'IFIELD IGNORE, "", "%ld"' >> ${TMPDIR}/datetime_iso.style
+
+  gpsbabel -i xcsv,style=${TMPDIR}/datetime_iso.style -f ${REFERENCE}/datetime~xcsv.xcsv -o xcsv,style=${TMPDIR}/datetime_iso.style -F ${TMPDIR}/datetime_iso.xcsv
+  compare ${REFERENCE}/datetime_read.xcsv ${TMPDIR}/datetime_iso.xcsv
+
+  echo 'FIELD_DELIMITER COMMA' > ${TMPDIR}/datetime_isoms.style
+  echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/datetime_isoms.style
+  echo 'PROLOGUE LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME' >> ${TMPDIR}/datetime_isoms.style
+  echo 'OFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'OFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'OFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD IGNORE, "", "%.12e"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD ISO_TIME_MS, "", "%s"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_isoms.style
+  echo 'IFIELD IGNORE, "", "%ld"' >> ${TMPDIR}/datetime_isoms.style
+
+  gpsbabel -i xcsv,style=${TMPDIR}/datetime_isoms.style -f ${REFERENCE}/datetime~xcsv.xcsv -o xcsv,style=${TMPDIR}/datetime_isoms.style -F ${TMPDIR}/datetime_isoms.xcsv
+  compare ${REFERENCE}/datetime_read.xcsv ${TMPDIR}/datetime_isoms.xcsv
+
+  echo 'FIELD_DELIMITER COMMA' > ${TMPDIR}/datetime_local.style
+  echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/datetime_local.style
+  echo 'PROLOGUE LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME' >> ${TMPDIR}/datetime_local.style
+  echo 'OFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_local.style
+  echo 'OFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_local.style
+  echo 'OFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD IGNORE, "", "%.12e"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD LOCAL_TIME, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_local.style
+  echo 'IFIELD IGNORE, "", "%ld"' >> ${TMPDIR}/datetime_local.style
+
+  gpsbabel -i xcsv,style=${TMPDIR}/datetime_local.style -f ${REFERENCE}/datetime~xcsv.xcsv -o xcsv,style=${TMPDIR}/datetime_local.style -F ${TMPDIR}/datetime_local.xcsv
+  compare ${REFERENCE}/datetime_read.xcsv ${TMPDIR}/datetime_local.xcsv
+
+  echo 'FIELD_DELIMITER COMMA' > ${TMPDIR}/datetime_net.style
+  echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/datetime_net.style
+  echo 'PROLOGUE LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME' >> ${TMPDIR}/datetime_net.style
+  echo 'OFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_net.style
+  echo 'OFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_net.style
+  echo 'OFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD IGNORE, "", "%.12e"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD NET_TIME, "", "%lld"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_net.style
+  echo 'IFIELD IGNORE, "", "%ld"' >> ${TMPDIR}/datetime_net.style
+
+  gpsbabel -i xcsv,style=${TMPDIR}/datetime_net.style -f ${REFERENCE}/datetime~xcsv.xcsv -o xcsv,style=${TMPDIR}/datetime_net.style -F ${TMPDIR}/datetime_net.xcsv
+  compare ${REFERENCE}/datetime_read.xcsv ${TMPDIR}/datetime_net.xcsv
+
+  echo 'FIELD_DELIMITER COMMA' > ${TMPDIR}/datetime_timet.style
+  echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/datetime_timet.style
+  echo 'PROLOGUE LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME' >> ${TMPDIR}/datetime_timet.style
+  echo 'OFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_timet.style
+  echo 'OFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_timet.style
+  echo 'OFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD IGNORE, "", "%.12e"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD TIMET_TIME, "", "%lld"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_timet.style
+  echo 'IFIELD IGNORE, "", "%ld"' >> ${TMPDIR}/datetime_timet.style
+
+  gpsbabel -i xcsv,style=${TMPDIR}/datetime_timet.style -f ${REFERENCE}/datetime~xcsv.xcsv -o xcsv,style=${TMPDIR}/datetime_timet.style -F ${TMPDIR}/datetime_timet.xcsv
+  compare ${REFERENCE}/datetime_read.xcsv ${TMPDIR}/datetime_timet.xcsv
+
+  echo 'FIELD_DELIMITER COMMA' > ${TMPDIR}/datetime_timetms.style
+  echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/datetime_timetms.style
+  echo 'PROLOGUE LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME' >> ${TMPDIR}/datetime_timetms.style
+  echo 'OFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'OFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'OFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD IGNORE, "", "%.12e"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD TIMET_TIME_MS, "", "%lld"' >> ${TMPDIR}/datetime_timetms.style
+  echo 'IFIELD IGNORE, "", "%ld"' >> ${TMPDIR}/datetime_timetms.style
+
+  gpsbabel -i xcsv,style=${TMPDIR}/datetime_timetms.style -f ${REFERENCE}/datetime~xcsv.xcsv -o xcsv,style=${TMPDIR}/datetime_timetms.style -F ${TMPDIR}/datetime_timetms.xcsv
+  compare ${REFERENCE}/datetime_read.xcsv ${TMPDIR}/datetime_timetms.xcsv
+
+  echo 'FIELD_DELIMITER COMMA' > ${TMPDIR}/datetime_hmsg.style
+  echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'PROLOGUE LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'OFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'OFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'OFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD IGNORE, "", "%.12e"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD GMT_TIME, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD GMT_TIME, "", "%Y-%m-%d"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_hmsg.style
+  echo 'IFIELD IGNORE, "", "%ld"' >> ${TMPDIR}/datetime_hmsg.style
+
+  gpsbabel -i xcsv,style=${TMPDIR}/datetime_hmsg.style -f ${REFERENCE}/datetime~xcsv.xcsv -o xcsv,style=${TMPDIR}/datetime_hmsg.style -F ${TMPDIR}/datetime_hmsg.xcsv
+  compare ${REFERENCE}/datetime_read.xcsv ${TMPDIR}/datetime_hmsg.xcsv
+
+  echo 'FIELD_DELIMITER COMMA' > ${TMPDIR}/datetime_hmsg2.style
+  echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'PROLOGUE LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'OFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'OFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'OFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD IGNORE, "", "%.12e"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD GMT_TIME, "", "%Y-%m-%d"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD GMT_TIME, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_hmsg2.style
+  echo 'IFIELD IGNORE, "", "%ld"' >> ${TMPDIR}/datetime_hmsg2.style
+
+  gpsbabel -i xcsv,style=${TMPDIR}/datetime_hmsg2.style -f ${REFERENCE}/datetime~xcsv.xcsv -o xcsv,style=${TMPDIR}/datetime_hmsg2.style -F ${TMPDIR}/datetime_hmsg2.xcsv
+  compare ${REFERENCE}/datetime_read.xcsv ${TMPDIR}/datetime_hmsg2.xcsv
+
+  echo 'FIELD_DELIMITER COMMA' > ${TMPDIR}/datetime_hmsl.style
+  echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'PROLOGUE LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'OFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'OFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'OFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD IGNORE, "", "%.12e"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD LOCAL_TIME, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD LOCAL_TIME, "", "%Y-%m-%d"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_hmsl.style
+  echo 'IFIELD IGNORE, "", "%ld"' >> ${TMPDIR}/datetime_hmsl.style
+
+  gpsbabel -i xcsv,style=${TMPDIR}/datetime_hmsl.style -f ${REFERENCE}/datetime~xcsv.xcsv -o xcsv,style=${TMPDIR}/datetime_hmsl.style -F ${TMPDIR}/datetime_hmsl.xcsv
+  compare ${REFERENCE}/datetime_read.xcsv ${TMPDIR}/datetime_hmsl.xcsv
+
+  echo 'FIELD_DELIMITER COMMA' > ${TMPDIR}/datetime_hmsl2.style
+  echo 'RECORD_DELIMITER NEWLINE' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'PROLOGUE LAT_DECIMAL,LON_DECIMAL,EXCEL_TIME,GMT_TIME-12HR,GMT_TIME-DATE-24HR,GMT_TIME-24HR,LOCAL_TIME-12HR,LOCAL_TIME-DATE-24HR,LOCAL_TIME-24HR,ISO_TIME,ISO_TIME_MS,NET_TIME,TIMET_TIME,TIMET_TIME_MS,YYYYMMDD_TIME' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'OFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'OFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'OFIELD ISO_TIME, "", "%s"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD LAT_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD LON_DECIMAL,"","%.6f"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD IGNORE, "", "%.12e"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD IGNORE, "", "%Y-%m-%d %H:%M:%S"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD IGNORE, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD IGNORE, "", "%I:%M:%S %p"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD LOCAL_TIME, "", "%Y-%m-%d"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD LOCAL_TIME, "", "%H:%M:%S"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD IGNORE, "", "%s"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD IGNORE, "", "%lld"' >> ${TMPDIR}/datetime_hmsl2.style
+  echo 'IFIELD IGNORE, "", "%ld"' >> ${TMPDIR}/datetime_hmsl2.style
+
+  gpsbabel -i xcsv,style=${TMPDIR}/datetime_hmsl2.style -f ${REFERENCE}/datetime~xcsv.xcsv -o xcsv,style=${TMPDIR}/datetime_hmsl2.style -F ${TMPDIR}/datetime_hmsl2.xcsv
+  compare ${REFERENCE}/datetime_read.xcsv ${TMPDIR}/datetime_hmsl2.xcsv
+
+  unset -v TZ
+fi
index 03c3147c47a8a4032919492a61f308963f89bfdf..480616934414240c10c40a0196c56643b09854cf 100644 (file)
--- a/unicsv.cc
+++ b/unicsv.cc
@@ -23,8 +23,7 @@
 
 #include <cmath>                   // for fabs, lround
 #include <cstdio>                  // for NULL, sscanf
-#include <cstring>                 // for strchr, strncpy
-#include <ctime>                   // for gmtime
+#include <ctime>                   // for tm
 
 #include <QByteArray>              // for QByteArray
 #include <QChar>                   // for QChar
@@ -225,7 +224,7 @@ UnicsvFormat::unicsv_parse_gc_code(const QString& str)
   return res;
 }
 
-time_t
+QDate
 UnicsvFormat::unicsv_parse_date(const char* str, int* consumed)
 {
   int p1, p2, p3;
@@ -240,9 +239,9 @@ UnicsvFormat::unicsv_parse_date(const char* str, int* consumed)
   if (ct != 5) {
     if (consumed) {            /* don't stop here; it's only sniffing */
       *consumed = 0;   /* for a possible date */
-      return 0;
+      return {};
     }
-    fatal(FatalMsg() << MYNAME << ": Could not parse date string (" << str << ").\n");
+    fatal(FatalMsg() << MYNAME << ": Could not parse date string (" << str << ").");
   }
 
   if ((p1 > 99) || (sep[0] == '-')) { /* Y-M-D (iso like) */
@@ -269,56 +268,59 @@ UnicsvFormat::unicsv_parse_date(const char* str, int* consumed)
   if ((tm.tm_mon > 12) || (tm.tm_mon < 1) || (tm.tm_mday > 31) || (tm.tm_mday < 1)) {
     if (consumed) {
       *consumed = 0;
-      return 0;        /* don't stop here */
+      return {};       /* don't stop here */
     }
-    fatal(FatalMsg() << MYNAME << ": Could not parse date string (" << str << ").\n");
+    fatal(FatalMsg() << MYNAME << ": Could not parse date string (" << str << ").");
   }
 
-  tm.tm_year -= 1900;
-  tm.tm_mon -= 1;
-
-  return mkgmtime(&tm);
+  QDate result{tm.tm_year, tm.tm_mon, tm.tm_mday};
+  if (!result.isValid()) {
+    fatal(FatalMsg() << MYNAME << ": Invalid date parsed from string (" << str << ").");
+  }
+  return result;
 }
 
-time_t
-UnicsvFormat::unicsv_parse_time(const char* str, int* usec, time_t* date)
+QTime
+UnicsvFormat::unicsv_parse_time(const char* str, QDate& date)
 {
-  int hour, min, sec;
+  int hour;
+  int min;
+  int sec;
+  int msec;
   int consumed = 0;
-  double us;
-  char sep[2];
+  double frac_sec;
 
   /* If we have something we're pretty sure is a date, parse that
    * first, skip over it, and pass that back to the caller)
    */
-  time_t ldate = unicsv_parse_date(str, &consumed);
-  if (consumed && ldate) {
+  QDate ldate = unicsv_parse_date(str, &consumed);
+  if (consumed && ldate.isValid()) {
     str += consumed;
-    if (date) {
-      *date = ldate;
-    }
+    date = ldate;
   }
-  int ct = sscanf(str, "%d%1[.://]%d%1[.://]%d%lf", &hour, sep, &min, sep, &sec, &us);
-  if (ct < 5) {
-    fatal(MYNAME ": Could not parse time string (%s).\n", str);
+  int ct = sscanf(str, "%d%*1[.://]%d%*1[.://]%d%lf", &hour, &min, &sec, &frac_sec);
+  if (ct < 3) {
+    fatal(FatalMsg() << MYNAME << ": Could not parse time string (" << str << ").");
   }
-  if (ct == 6) {
-    *usec = lround((us * 1000000));
-    if (*usec > 999999) {
-      *usec = 0;
-      sec++;
-    }
+  if (ct >= 4) {
+    // Don't round up and ripple through seconds, minutes, hours.
+    // 23:59:59.9999999 -> 24:00:00.000 which is an invalid QTime.
+    msec = frac_sec * 1000.0;
   } else {
-    *usec = 0;
+    msec = 0;
   }
 
-  return ((hour * SECONDS_PER_HOUR) + (min * 60) + sec);
+  QTime result{hour, min, sec, msec};
+  if (!result.isValid()) {
+    fatal(FatalMsg() << MYNAME << ": Invalid time parsed from string (" << str << ").");
+  }
+  return result;
 }
 
-time_t
-UnicsvFormat::unicsv_parse_time(const QString& str, int* msec, time_t* date)
+QTime
+UnicsvFormat::unicsv_parse_time(const QString& str, QDate& date)
 {
-  return unicsv_parse_time(CSTR(str), msec, date);
+  return unicsv_parse_time(CSTR(str), date);
 }
 
 Geocache::status_t
@@ -338,19 +340,9 @@ UnicsvFormat::unicsv_parse_status(const QString& str)
 }
 
 QDateTime
-UnicsvFormat::unicsv_adjust_time(const time_t time, const time_t* date) const
+UnicsvFormat::unicsv_adjust_time(const QDate date, const QTime time, bool is_localtime) const
 {
-  time_t res = time;
-  if (date) {
-    res += *date;
-  }
-  if (opt_utc) {
-    res += xstrtoi(opt_utc, nullptr, 10) * SECONDS_PER_HOUR;
-  } else {
-    std::tm tm = *gmtime(&res);
-    res = mklocaltime(&tm);
-  }
-  return QDateTime::fromSecsSinceEpoch(res, Qt::UTC);
+  return make_datetime(date, time, is_localtime, opt_utc != nullptr, utc_offset);
 }
 
 bool
@@ -470,6 +462,8 @@ UnicsvFormat::rd_init(const QString& fname)
   } else {
     unicsv_fieldsep = nullptr;
   }
+
+  utc_offset = (opt_utc == nullptr)? 0 : xstrtoi(opt_utc, nullptr, 10) * SECONDS_PER_HOUR;
 }
 
 void
@@ -495,10 +489,11 @@ UnicsvFormat::unicsv_parse_one_line(const QString& ibuf)
   double swiss_easting = kUnicsvUnknown;
   double swiss_northing = kUnicsvUnknown;
   int checked = 0;
-  time_t date = -1;
-  time_t time = -1;
-  int usec = -1;
-  char is_localtime = 0;
+  QDate local_date;
+  QTime local_time;
+  QDate utc_date;
+  QTime utc_time;
+  bool need_datetime = true;
   garmin_fs_t* gmsd;
   double d;
   std::tm ymd{};
@@ -682,16 +677,14 @@ UnicsvFormat::unicsv_parse_one_line(const QString& ibuf)
       break;
 
     case fld_utc_date:
-      if ((is_localtime < 2) && (date < 0)) {
-        date = unicsv_parse_date(CSTR(value), nullptr);
-        is_localtime = 0;
+      if (need_datetime && !utc_date.isValid()) {
+        utc_date = unicsv_parse_date(CSTR(value), nullptr);
       }
       break;
 
     case fld_utc_time:
-      if ((is_localtime < 2) && (time < 0)) {
-        time = unicsv_parse_time(value, &usec, &date);
-        is_localtime = 0;
+      if (need_datetime && !utc_time.isValid()) {
+        utc_time = unicsv_parse_time(value, utc_date);
       }
       break;
 
@@ -763,21 +756,19 @@ UnicsvFormat::unicsv_parse_one_line(const QString& ibuf)
       break;
 
     case fld_iso_time:
-      is_localtime = 2;        /* fix result */
-      wpt->SetCreationTime(xml_parse_time(value));
+      need_datetime = false;   /* fix result */
+      wpt->SetCreationTime(QDateTime::fromString(value, Qt::ISODateWithMs));
       break;
 
     case fld_time:
-      if ((is_localtime < 2) && (time < 0)) {
-        time = unicsv_parse_time(value, &usec, &date);
-        is_localtime = 1;
+      if (need_datetime && !local_time.isValid()) {
+        local_time = unicsv_parse_time(value, local_date);
       }
       break;
 
     case fld_date:
-      if ((is_localtime < 2) && (date < 0)) {
-        date = unicsv_parse_date(CSTR(value), nullptr);
-        is_localtime = 1;
+      if (need_datetime && !local_date.isValid()) {
+        local_date = unicsv_parse_date(CSTR(value), nullptr);
       }
       break;
 
@@ -806,9 +797,8 @@ UnicsvFormat::unicsv_parse_one_line(const QString& ibuf)
       break;
 
     case fld_datetime:
-      if ((is_localtime < 2) && (date < 0) && (time < 0)) {
-        time = unicsv_parse_time(value, &usec, &date);
-        is_localtime = 1;
+      if (need_datetime && !local_date.isValid() && !local_time.isValid()) {
+        local_time = unicsv_parse_time(value, local_date);
       }
       break;
 
@@ -918,20 +908,20 @@ UnicsvFormat::unicsv_parse_one_line(const QString& ibuf)
         gc_data->is_available = unicsv_parse_status(value);
         break;
       case fld_gc_exported: {
-        time_t etime, edate;
-        int eusec;
-        etime = unicsv_parse_time(value, &eusec, &edate);
-        if (edate || etime) {
-          gc_data->exported = unicsv_adjust_time(etime, &edate);
+        QTime etime;
+        QDate edate;
+        etime = unicsv_parse_time(value, edate);
+        if (edate.isValid() || etime.isValid()) {
+          gc_data->exported = unicsv_adjust_time(edate, etime, true);
         }
       }
       break;
       case fld_gc_last_found: {
-        time_t ftime, fdate;
-        int fusec;
-        ftime = unicsv_parse_time(value, &fusec, &fdate);
-        if (fdate || ftime) {
-          gc_data->last_found = unicsv_adjust_time(ftime, &fdate);
+        QTime ftime;
+        QDate fdate;
+        ftime = unicsv_parse_time(value, fdate);
+        if (fdate.isValid() || ftime.isValid()) {
+          gc_data->last_found = unicsv_adjust_time(fdate, ftime, true);
         }
       }
       break;
@@ -960,24 +950,19 @@ UnicsvFormat::unicsv_parse_one_line(const QString& ibuf)
     return;
   }
 
-  if (is_localtime < 2) {      /* not fixed */
-    if ((time >= 0) && (date >= 0)) {
-      time_t t = date + time;
-
-      if (is_localtime) {
-        std::tm tm = *gmtime(&t);
-        if (opt_utc) {
-          wpt->SetCreationTime(mkgmtime(&tm));
-        } else {
-          wpt->SetCreationTime(mklocaltime(&tm));
-        }
-      } else {
-        wpt->SetCreationTime(t);
-      }
-    } else if (time >= 0) {
-      wpt->SetCreationTime(time);
-    } else if (date >= 0) {
-      wpt->SetCreationTime(date);
+  if (need_datetime) { /* not fixed */
+    if (utc_date.isValid() && utc_time.isValid()) {
+      wpt->SetCreationTime(unicsv_adjust_time(utc_date, utc_time, false));
+    } else if (local_date.isValid() && local_time.isValid()) {
+      wpt->SetCreationTime(unicsv_adjust_time(local_date, local_time, true));
+    } else if (utc_date.isValid()) {
+      wpt->SetCreationTime(unicsv_adjust_time(utc_date, utc_time, false));
+    } else if (local_date.isValid()) {
+      wpt->SetCreationTime(unicsv_adjust_time(local_date, local_time, true));
+    } else if (utc_time.isValid()) {
+      wpt->SetCreationTime(unicsv_adjust_time(utc_date, utc_time, false));
+    } else if (local_time.isValid()) {
+      wpt->SetCreationTime(unicsv_adjust_time(local_date, local_time, true));
     } else if (ymd.tm_year || ymd.tm_mon || ymd.tm_mday) {
       if (ymd.tm_year < 100) {
         if (ymd.tm_year <= 70) {
@@ -986,7 +971,6 @@ UnicsvFormat::unicsv_parse_one_line(const QString& ibuf)
           ymd.tm_year += 1900;
         }
       }
-      ymd.tm_year -= 1900;
 
       if (ymd.tm_mon == 0) {
         ymd.tm_mon = 1;
@@ -995,27 +979,17 @@ UnicsvFormat::unicsv_parse_one_line(const QString& ibuf)
         ymd.tm_mday = 1;
       }
 
-      ymd.tm_mon--;
-      if (opt_utc) {
-        wpt->SetCreationTime(mkgmtime(&ymd));
-      } else {
-        wpt->SetCreationTime(mklocaltime(&ymd));
-      }
+      wpt->SetCreationTime(unicsv_adjust_time(
+                           QDate(ymd.tm_year, ymd.tm_mon, ymd.tm_mday),
+                           QTime(ymd.tm_hour, ymd.tm_min, ymd.tm_sec),
+                           true));
     } else if (ymd.tm_hour || ymd.tm_min || ymd.tm_sec) {
-      if (opt_utc) {
-        wpt->SetCreationTime(mkgmtime(&ymd));
-      } else {
-        wpt->SetCreationTime(mklocaltime(&ymd));
-      }
-    }
-
-    if (usec >= 0) {
-      wpt->creation_time = wpt->creation_time.addMSecs(MICRO_TO_MILLI(usec));
+      wpt->SetCreationTime(unicsv_adjust_time(
+                           QDate(),
+                           QTime(ymd.tm_hour, ymd.tm_min, ymd.tm_sec),
+                           true));
     }
 
-    if (opt_utc) {
-      wpt->creation_time = wpt->creation_time.addSecs(xstrtoi(opt_utc, nullptr, 10) * SECONDS_PER_HOUR);
-    }
   }
 
   /* utm/bng/swiss can be optional */
@@ -1131,16 +1105,16 @@ UnicsvFormat::unicsv_print_str(const QString& s) const
 }
 
 void
-UnicsvFormat::unicsv_print_data_time(const QDateTime& idt) const
+UnicsvFormat::unicsv_print_date_time(const QDateTime& idt) const
 {
   if (!idt.isValid()) {
     return;
   }
-  QDateTime dt = idt;
-  if (opt_utc) {
-    //time += xstrtoi(opt_utc, nullptr, 10) * SECONDS_PER_HOUR;
-    dt = dt.addSecs(xstrtoi(opt_utc, nullptr, 10) * SECONDS_PER_HOUR);
-    dt = dt.toUTC();
+  QDateTime dt;
+  if (opt_utc != nullptr) {
+    dt = idt.toOffsetFromUtc(utc_offset);
+  } else {
+    dt = idt.toLocalTime();
   }
 
   unicsv_print_str(dt.toString(u"yyyy/MM/dd hh:mm:ss"));
@@ -1174,7 +1148,7 @@ UnicsvFormat::unicsv_waypt_enum_cb(const Waypoint* wpt)
   }
   if (wpt->creation_time.isValid()) {
     unicsv_outp_flags[fld_time] = true;
-    if (wpt->creation_time.toTime_t() >= SECONDS_PER_DAY) {
+    if (wpt->creation_time.toTime_t() >= 2 * SECONDS_PER_DAY) {
       unicsv_outp_flags[fld_date] = true;
     }
   }
@@ -1524,12 +1498,10 @@ UnicsvFormat::unicsv_waypt_disp_cb(const Waypoint* wpt)
     }
   }
   if (unicsv_outp_flags[fld_date]) {
-    if (wpt->creation_time.toTime_t() >= SECONDS_PER_DAY) {
+    if (wpt->creation_time.toTime_t() >= 2 * SECONDS_PER_DAY) {
       QDateTime dt;
-      if (opt_utc) {
-        dt = wpt->GetCreationTime().toUTC();
-        // We might wrap to a different day by overriding the TZ offset.
-        dt = dt.addSecs(xstrtoi(opt_utc, nullptr, 10) * SECONDS_PER_HOUR);
+      if (opt_utc != nullptr) {
+        dt = wpt->GetCreationTime().toOffsetFromUtc(utc_offset);
       } else {
         dt = wpt->GetCreationTime().toLocalTime();
       }
@@ -1541,18 +1513,17 @@ UnicsvFormat::unicsv_waypt_disp_cb(const Waypoint* wpt)
   }
   if (unicsv_outp_flags[fld_time]) {
     if (wpt->creation_time.isValid()) {
-      QTime t;
-      if (opt_utc) {
-        t = wpt->GetCreationTime().toUTC().time();
-        t = t.addSecs(xstrtoi(opt_utc, nullptr, 10) * SECONDS_PER_HOUR);
+      QDateTime dt;
+      if (opt_utc != nullptr) {
+        dt = wpt->GetCreationTime().toOffsetFromUtc(utc_offset);
       } else {
-        t = wpt->GetCreationTime().toLocalTime().time();
+        dt = wpt->GetCreationTime().toLocalTime();
       }
       QString out;
-      if (t.msec() > 0) {
-        out = t.toString(u"hh:mm:ss.zzz");
+      if (dt.time().msec() > 0) {
+        out = dt.toString(u"hh:mm:ss.zzz");
       } else {
-        out = t.toString(u"hh:mm:ss");
+        out = dt.toString(u"hh:mm:ss");
       }
       *fout << unicsv_fieldsep << out;
     } else {
@@ -1653,14 +1624,14 @@ UnicsvFormat::unicsv_waypt_disp_cb(const Waypoint* wpt)
   }
   if (unicsv_outp_flags[fld_gc_exported]) {
     if (gc_data) {
-      unicsv_print_data_time(gc_data->exported);
+      unicsv_print_date_time(gc_data->exported);
     } else {
       *fout << unicsv_fieldsep;
     }
   }
   if (unicsv_outp_flags[fld_gc_last_found]) {
     if (gc_data) {
-      unicsv_print_data_time(gc_data->last_found);
+      unicsv_print_date_time(gc_data->last_found);
     } else {
       *fout << unicsv_fieldsep;
     }
@@ -1742,6 +1713,7 @@ UnicsvFormat::wr_init(const QString& fname)
   }
 
   llprec = xstrtoi(opt_prec, nullptr, 10);
+  utc_offset = (opt_utc == nullptr)? 0 : xstrtoi(opt_utc, nullptr, 10) * SECONDS_PER_HOUR;
 }
 
 void
index 8a8c8aac6a4a047ac84b74221fbe6749f9ea3c3a..ce564508d89beac26294a1a6f487295ad86f4704 100644 (file)
--- a/unicsv.h
+++ b/unicsv.h
 
 #include <bitset>                 // for bitset
 #include <cstdint>                // for uint32_t
-#include <ctime>                  // for gmtime
 
+#include <QDate>                  // for QDate
 #include <QDateTime>              // for QDateTime
 #include <QString>                // for QString
+#include <QTime>                  // for QTime
 #include <QVector>                // for QVector
 
 #include "defs.h"
@@ -165,17 +166,17 @@ private:
   /* Member Functions */
 
   static long long int unicsv_parse_gc_code(const QString& str);
-  static time_t unicsv_parse_date(const char* str, int* consumed);
-  static time_t unicsv_parse_time(const char* str, int* usec, time_t* date);
-  static time_t unicsv_parse_time(const QString& str, int* msec, time_t* date);
+  static QDate unicsv_parse_date(const char* str, int* consumed);
+  static QTime unicsv_parse_time(const char* str, QDate& date);
+  static QTime unicsv_parse_time(const QString& str, QDate& date);
   static Geocache::status_t unicsv_parse_status(const QString& str);
-  QDateTime unicsv_adjust_time(time_t time, const time_t* date) const;
+  QDateTime unicsv_adjust_time(const QDate date, const QTime time, bool is_localtime) const;
   static bool unicsv_compare_fields(const QString& s, const field_t* f);
   void unicsv_fondle_header(QString header);
   void unicsv_parse_one_line(const QString& ibuf);
   void unicsv_fatal_outside(const Waypoint* wpt) const;
   void unicsv_print_str(const QString& s) const;
-  void unicsv_print_data_time(const QDateTime& idt) const;
+  void unicsv_print_date_time(const QDateTime& idt) const;
   void unicsv_waypt_enum_cb(const Waypoint* wpt);
   void unicsv_waypt_disp_cb(const Waypoint* wpt);
   static void unicsv_check_modes(bool test);
@@ -208,6 +209,7 @@ private:
   int unicsv_waypt_ct{};
   char unicsv_detect{};
   int llprec{};
+  int utc_offset{};
 
   QVector<arglist_t> unicsv_args = {
     {
@@ -220,7 +222,7 @@ private:
     },
     {
       "utc",   &opt_utc,   "Write timestamps with offset x to UTC time",
-      nullptr, ARGTYPE_INT, "-23", "+23", nullptr
+      nullptr, ARGTYPE_INT, "-14", "+14", nullptr
     },
     {
       "format", &opt_format,   "Write name(s) of format(s) from input session(s)",
diff --git a/util.cc b/util.cc
index 598891d76fe56a6a19708ec6fce56a235f2735b5..3495491953d2f86a0744de342ba6a36de7e86bd7 100644 (file)
--- a/util.cc
+++ b/util.cc
 #include <cmath>                        // for fabs, floor
 #include <cstdarg>                      // for va_list, va_end, va_start, va_copy
 #include <cstdio>                       // for size_t, vsnprintf, FILE, fopen, printf, sprintf, stderr, stdin, stdout
-#include <cstdint>                      // for uint32_t
 #include <cstdlib>                      // for abs, calloc, free, malloc, realloc
 #include <cstring>                      // for strlen, strcat, strstr, memcpy, strcmp, strcpy, strdup, strchr, strerror
-#include <ctime>                        // for mktime, localtime
 
 #include <QByteArray>                   // for QByteArray
 #include <QChar>                        // for QChar, operator<=, operator>=
@@ -447,67 +445,46 @@ le_write32(void* ptr, const unsigned value)
   p[3] = value >> 24;
 }
 
-/*
-       mkgmtime -- convert tm struct in UTC to time_t
-
-       works just like mktime but without all the mucking
-       around with timezones and daylight savings
-
-       Borrowed from lynx GPL source code
-       http://lynx.isc.org/release/lynx2-8-5/src/mktime.c
-
-       Written by Philippe De Muyter <phdm@macqel.be>.
-*/
-
-time_t
-mkgmtime(std::tm* time)
+QDateTime
+make_datetime(QDate date, QTime time, bool is_localtime, bool force_utc, int utc_offset)
 {
-  static const int      m_to_d[12] =
-  {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
-
-  short month = time->tm_mon;
-  short year = time->tm_year + month / 12 + 1900;
-  month %= 12;
-  if (month < 0) {
-    year -= 1;
-    month += 12;
-  }
-  time_t result = (year - 1970) * 365 + m_to_d[month];
-  if (month <= 1) {
-    year -= 1;
+  QDateTime result;
+  Qt::TimeSpec timespec;
+  int offset = 0;
+
+  if (is_localtime) {
+    if (force_utc) { // override with passed option value
+      if (utc_offset == 0) {
+        // Qt 6.5.0 QDate::startOfDay(Qt::OffsetFromUTC, 0) returns an invalid QDateTime.
+        timespec = Qt::UTC;
+      } else {
+        timespec = Qt::OffsetFromUTC;
+        // Avoid Qt 6.5.0 warnings with non-zero offsets when not using Qt::OffsetFromUTC.
+        offset = utc_offset;
+      }
+    } else {
+      timespec = Qt::LocalTime;
+    }
+  } else {
+    timespec = Qt::UTC;
   }
-  result += (year - 1968) / 4;
-  result -= (year - 1900) / 100;
-  result += (year - 1600) / 400;
-  result += time->tm_mday;
-  result -= 1;
-  result *= 24;
-  result += time->tm_hour;
-  result *= 60;
-  result += time->tm_min;
-  result *= 60;
-  result += time->tm_sec;
-  return (result);
-}
 
-/*
- * mklocaltime: same as mktime, but try to recover the "Summer time flag",
- *              which is evaluated by mktime
- */
-time_t
-mklocaltime(std::tm* time)
-{
-  time_t result;
-  std::tm check = *time;
-
-  check.tm_isdst = 0;
-  result = mktime(&check);
-  check = *localtime(&result);
-  if (check.tm_isdst == 1) {   /* DST is in effect */
-    check = *time;
-    check.tm_isdst = 1;
-    result = mktime(&check);
+  if (date.isValid() && time.isValid()) {
+    result = QDateTime(date, time, timespec, offset);
+  } else if (time.isValid()) {
+    // TODO: Wouldn't it be better to return an invalid QDateTime
+    // that contained an invalid QDate, a valid QTime and a valid
+    // Qt::TimeSpec?
+    result = QDateTime(QDate(1970, 1, 1), time, timespec, offset);
+  } else if (date.isValid()) {
+    //  no time, use start of day in the given Qt::TimeSpec.
+#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0))
+    result = QDateTime(date, QTime(0,0), timespec, offset);
+#else
+    result = date.startOfDay(timespec, offset);
+#endif
   }
   return result;
 }
 
@@ -548,6 +525,13 @@ QDateTime dotnet_time_to_qdatetime(long long dotnet)
   return epoch.addMSecs(millisecs);
 }
 
+long long qdatetime_to_dotnet_time(const QDateTime& dt)
+{
+  QDateTime epoch = QDateTime(QDate(1, 1, 1), QTime(0, 0, 0), Qt::UTC);
+  qint64 millisecs = epoch.msecsTo(dt);
+  return millisecs * 10000;
+}
+
 double
 endian_read_double(const void* ptr, int read_le)
 {
diff --git a/xcsv.cc b/xcsv.cc
index 2d42b6f6b41b5282a880176a25eb223a66c0f751..fa7e6dbdcd15e1666a7ec72ab53f98db578c9813 100644 (file)
--- a/xcsv.cc
+++ b/xcsv.cc
@@ -28,6 +28,7 @@
 #include <cctype>                  // for isdigit, tolower
 #include <cmath>                   // for fabs, pow
 #include <cstdio>                  // for snprintf, sscanf
+#include <cstdint>                 // for uint32_t
 #include <cstdlib>                 // for strtod
 #include <cstring>                 // for strlen, strncmp, strcmp
 #include <ctime>                   // for gmtime, localtime, time_t, mktime, strftime
@@ -96,8 +97,6 @@ const QHash<QString, XcsvStyle::xcsv_token> XcsvStyle::xcsv_tokens {
   { "GPS_SAT", XT_GPS_SAT },
   { "GPS_VDOP", XT_GPS_VDOP },
   { "HEART_RATE", XT_HEART_RATE },
-  { "HMSG_TIME", XT_HMSG_TIME },
-  { "HMSL_TIME", XT_HMSL_TIME },
   { "ICON_DESCR", XT_ICON_DESCR },
   { "IGNORE", XT_IGNORE },
   { "INDEX", XT_INDEX },
@@ -245,67 +244,82 @@ XcsvStyle::xcsv_ofield_add(XcsvStyle* style, const QString& qkey, const QString&
   style->ofields.append(fmp);
 }
 
-QDateTime
+QDate
 XcsvFormat::yyyymmdd_to_time(const QString& s)
 {
-  QDate d = QDate::fromString(s, "yyyyMMdd");
-#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0))
-  return QDateTime(d);
-#else
-  return d.startOfDay();
-#endif
+  return QDate::fromString(s, "yyyyMMdd");
 }
 
+QDateTime
+XcsvFormat::xcsv_adjust_time(const QDate date, const QTime time, bool is_localtime) const
+{
+  return make_datetime(date, time, is_localtime, opt_utc != nullptr, utc_offset);
+}
 
 /*
  * sscanftime - Parse a date buffer using strftime format
  */
-time_t
-XcsvFormat::sscanftime(const char* s, const char* format, bool gmt)
+void
+XcsvFormat::sscanftime(const char* s, const char* format, QDate& date, QTime& time)
 {
   std::tm stm{};
+  stm.tm_sec = -1;
+  stm.tm_min = -1;
+  stm.tm_hour = -1;
+  stm.tm_mday = -1;
+  stm.tm_mon = -1;
+  stm.tm_year = -1;
+  stm.tm_wday = -1;
+  stm.tm_yday = -1;
+  stm.tm_isdst = -1;
 
   if (strptime(s, format, &stm)) {
-    if ((stm.tm_mday == 0) && (stm.tm_mon == 0) && (stm.tm_year == 0)) {
-      stm.tm_mday = 1;
-      stm.tm_mon = 0;
-      stm.tm_year = 70;
-    }
-    stm.tm_isdst = -1;
-    if (gmt) {
-      return mkgmtime(&stm);
-    } else {
-      return mktime(&stm);
+
+    std::optional<QTime> time_result;
+    bool bad_time_parse = false;
+    if (stm.tm_hour >= 0 && stm.tm_min >= 0 && stm.tm_sec >= 0) {
+      time_result = QTime(stm.tm_hour, stm.tm_min, stm.tm_sec);
+    } else if (stm.tm_hour >= 0 && stm.tm_min >= 0) {
+      time_result = QTime(stm.tm_hour, stm.tm_min, 0);
+    } else if (stm.tm_hour >= 0) {
+      time_result = QTime(stm.tm_hour, 0, 0);
+    } else if (!(stm.tm_hour == -1 && stm.tm_min == -1 && stm.tm_sec == -1)) {
+      bad_time_parse = true;
     }
-  }
-  // Don't fuss for empty strings.
-  if (*s) {
-    warning("date parse of string '%s' with format '%s' failed.\n",
+    if ((time_result.has_value() && !time_result->isValid()) || bad_time_parse) {
+      fatal(MYNAME ": couldn't parse time from string '%s' with format '%s'.\n",
             s, format);
-  }
-  return 0;
-}
+    }
+    if (time_result.has_value()) {
+      time = *time_result;
+    }
 
-time_t
-XcsvFormat::addhms(const char* s, const char* format)
-{
-  time_t tt = 0;
-  int hour = 0;
-  int min = 0;
-  int sec = 0;
-
-  char* ampm = (char*) xmalloc(strlen(s) + 1);
-  int ac = sscanf(s, format, &hour, &min, &sec, ampm);
-  /* If no time format in arg string, assume AM */
-  if (ac < 4) {
-    ampm[0] = 0;
-  }
-  if (ac) {
-    tt = ((tolower(ampm[0])=='p') ? 43200 : 0) + 3600 * hour + 60 * min + sec;
+    std::optional<QDate> date_result;
+    bool bad_date_parse = false;
+    int year_result = (stm.tm_year >= 70)? stm.tm_year + 1900 : stm.tm_year + 2000;
+    if (stm.tm_year >= 0 && stm.tm_mon >= 0 && stm.tm_mday >= 0) {
+      date_result = QDate(year_result, stm.tm_mon + 1, stm.tm_mday);
+    } else if (stm.tm_year >= 0 && stm.tm_mon >= 0) {
+      date_result = QDate(year_result, stm.tm_mon + 1, 1);
+    } else if (stm.tm_year >= 0) {
+      date_result = QDate(year_result, 1, 1);
+    } else if (!(stm.tm_year == -1 && stm.tm_mon == -1 && stm.tm_mday == -1)) {
+      bad_date_parse = true;
+    }
+    if ((date_result.has_value() && !date_result->isValid()) || bad_date_parse) {
+      fatal(MYNAME ": couldn't parse date from string '%s' with format '%s'.\n",
+            s, format);
+    }
+    if (date_result.has_value()) {
+      date = *date_result;
+    }
+  } else {
+    // Don't fuss for empty strings.
+    if (*s) {
+      warning("date parse of string '%s' with format '%s' failed.\n",
+              s, format);
+    }
   }
-  xfree(ampm);
-
-  return tt;
 }
 
 QString
@@ -331,40 +345,14 @@ XcsvFormat::writetime(const char* format, time_t t, bool gmt)
 QString
 XcsvFormat::writetime(const char* format, const gpsbabel::DateTime& t, bool gmt)
 {
-  return writetime(format, t.toTime_t(), gmt);
-}
-
-QString
-XcsvFormat::writehms(const char* format, time_t t, bool gmt)
-{
-  static const std::tm no_time{};
-  static const std::tm* stmp = &no_time;
-
-  if (gmt) {
-    stmp = gmtime(&t);
-  } else {
-    stmp = localtime(&t);
-  }
-
-  if (stmp == nullptr) {
-    stmp = &no_time;
-  }
-
-  return QString::asprintf(format,
-                           stmp->tm_hour, stmp->tm_min, stmp->tm_sec,
-                           (stmp->tm_hour >= 12 ? "PM" : "AM"));
-}
-
-QString
-XcsvFormat::writehms(const char* format, const gpsbabel::DateTime& t, bool gmt)
-{
-  return writehms(format, t.toTime_t(), gmt);
+  uint32_t tt = t.toTime_t();
+  return (tt == 0xffffffffU)? QString() : writetime(format, tt, gmt);
 }
 
 long
 XcsvFormat::time_to_yyyymmdd(const QDateTime& t)
 {
-  QDate d = t.date();
+  QDate d = t.toUTC().date();
   return d.year() * 10000 + d.month() * 100 + d.day();
 }
 
@@ -594,15 +582,26 @@ XcsvFormat::xcsv_parse_val(const QString& value, Waypoint* wpt, const XcsvStyle:
     break;
 
   /* TIME CONVERSIONS ***************************************************/
-  case XcsvStyle::XT_EXCEL_TIME:
+  case XcsvStyle::XT_EXCEL_TIME: {
     /* Time as Excel Time  */
-    wpt->SetCreationTime(excel_to_timet(strtod(s, nullptr)));
-    break;
+    bool ok;
+    double et = value.toDouble(&ok);
+    if (ok) {
+      wpt->SetCreationTime(0, excel_to_timetms(et));
+      parse_data->need_datetime = false;
+    } else if (!value.isEmpty()) {
+      warning("parse of string '%s' on line number %d as EXCEL_TIME failed.\n", s, line_no);
+    }
+  }
+  break;
   case XcsvStyle::XT_TIMET_TIME: {
     /* Time as time_t */
     bool ok;
-    wpt->SetCreationTime(value.toLongLong(&ok));
-    if (!ok) {
+    long long tt = value.toLongLong(&ok);
+    if (ok) {
+      wpt->SetCreationTime(tt);
+      parse_data->need_datetime = false;
+    } else if (!value.isEmpty()) {
       warning("parse of string '%s' on line number %d as TIMET_TIME failed.\n", s, line_no);
     }
   }
@@ -610,47 +609,50 @@ XcsvFormat::xcsv_parse_val(const QString& value, Waypoint* wpt, const XcsvStyle:
   case XcsvStyle::XT_TIMET_TIME_MS: {
     /* Time as time_t in milliseconds */
     bool ok;
-    wpt->SetCreationTime(0, value.toLongLong(&ok));
-    if (!ok) {
+    long long tt = value.toLongLong(&ok);
+    if (ok) {
+      wpt->SetCreationTime(0, tt);
+      parse_data->need_datetime = false;
+    } else if (!value.isEmpty()) {
       warning("parse of string '%s' on line number %d as TIMET_TIME_MS failed.\n", s, line_no);
     }
   }
   break;
   case XcsvStyle::XT_YYYYMMDD_TIME:
-    wpt->SetCreationTime(yyyymmdd_to_time(value));
+    parse_data->utc_date = yyyymmdd_to_time(value);
     break;
   case XcsvStyle::XT_GMT_TIME:
-    wpt->SetCreationTime(sscanftime(s, fmp.printfc.constData(), true));
+    sscanftime(s, fmp.printfc.constData(), parse_data->utc_date, parse_data->utc_time);
     break;
   case XcsvStyle::XT_LOCAL_TIME:
-    if (!gpsbabel_testmode()) {
-      wpt->creation_time = wpt->creation_time.addSecs(sscanftime(s, fmp.printfc.constData(), false));
-    } else {
-      /* Force constant time zone for test */
-      wpt->creation_time = wpt->creation_time.addSecs(sscanftime(s, fmp.printfc.constData(), true));
-    }
-    break;
-  /* Useful when time and date are in separate fields
-       GMT / Local offset is handled by the two cases above */
-  case XcsvStyle::XT_HMSG_TIME:
-  case XcsvStyle::XT_HMSL_TIME:
-    wpt->creation_time = wpt->creation_time.addSecs(addhms(s, fmp.printfc.constData()));
+    sscanftime(s, fmp.printfc.constData(), parse_data->local_date, parse_data->local_time);
     break;
   case XcsvStyle::XT_ISO_TIME:
   case XcsvStyle::XT_ISO_TIME_MS:
-    wpt->SetCreationTime(xml_parse_time(value));
+    wpt->SetCreationTime(QDateTime::fromString(value, Qt::ISODateWithMs));
+    parse_data->need_datetime = false;
     break;
   case XcsvStyle::XT_NET_TIME: {
     bool ok;
-    wpt->SetCreationTime(dotnet_time_to_qdatetime(value.toLongLong(&ok)));
-    if (!ok) {
+    long long dnt = value.toLongLong(&ok);
+    if (ok) {
+      wpt->SetCreationTime(dotnet_time_to_qdatetime(dnt));
+      parse_data->need_datetime = false;
+    } else if (!value.isEmpty()) {
       warning("parse of string '%s' on line number %d as NET_TIME failed.\n", s, line_no);
     }
   }
   break;
-  case XcsvStyle::XT_GEOCACHE_LAST_FOUND:
-    wpt->AllocGCData()->last_found = yyyymmdd_to_time(value);
+  case XcsvStyle::XT_GEOCACHE_LAST_FOUND: {
+    QDate date;
+    date = yyyymmdd_to_time(value);
+#if (QT_VERSION < QT_VERSION_CHECK(5, 14, 0))
+    wpt->AllocGCData()->last_found = QDateTime(date);
+#else
+    wpt->AllocGCData()->last_found = date.startOfDay();
+#endif
     break;
+  }
 
   /* GEOCACHING STUFF ***************************************************/
   case XcsvStyle::XT_GEOCACHE_DIFF:
@@ -882,6 +884,23 @@ XcsvFormat::read()
         }
       }
 
+
+      if (parse_data.need_datetime) {
+        if (parse_data.utc_date.isValid() && parse_data.utc_time.isValid()) {
+          wpt_tmp->SetCreationTime(xcsv_adjust_time(parse_data.utc_date, parse_data.utc_time, false));
+        } else if (parse_data.local_date.isValid() && parse_data.local_time.isValid()) {
+          wpt_tmp->SetCreationTime(xcsv_adjust_time(parse_data.local_date, parse_data.local_time, true));
+        } else if (parse_data.utc_date.isValid()) {
+          wpt_tmp->SetCreationTime(xcsv_adjust_time(parse_data.utc_date, parse_data.utc_time, false));
+        } else if (parse_data.local_date.isValid()) {
+          wpt_tmp->SetCreationTime(xcsv_adjust_time(parse_data.local_date, parse_data.local_time, true));
+        } else if (parse_data.utc_time.isValid()) {
+          wpt_tmp->SetCreationTime(xcsv_adjust_time(parse_data.utc_date, parse_data.utc_time, false));
+        } else if (parse_data.local_time.isValid()) {
+          wpt_tmp->SetCreationTime(xcsv_adjust_time(parse_data.local_date, parse_data.local_time, true));
+        }
+      }
+
       // If XT_LAT_DIR(XT_LON_DIR) was an input field, and the latitude(longitude) is positive,
       // assume the latitude(longitude) was the absolute value and take the sign from XT_LAT_DIR(XT_LON_DIR).
       if (parse_data.lat_dir_positive.has_value() && !(*parse_data.lat_dir_positive) && (wpt_tmp->latitude > 0.0)) {
@@ -1361,18 +1380,26 @@ XcsvFormat::xcsv_waypt_pr(const Waypoint* wpt)
     /* TIME CONVERSIONS**************************************************/
     case XcsvStyle::XT_EXCEL_TIME:
       /* creation time as an excel (double) time */
-      buff = QString::asprintf(fmp.printfc.constData(), timet_to_excel(wpt->GetCreationTime().toTime_t()));
+      if (wpt->GetCreationTime().isValid()) {
+        buff = QString::asprintf(fmp.printfc.constData(), timetms_to_excel(wpt->GetCreationTime().toMSecsSinceEpoch()));
+      }
       break;
     case XcsvStyle::XT_TIMET_TIME:
       /* time as a time_t variable in seconds */
-      buff = QString::asprintf(fmp.printfc.constData(), wpt->GetCreationTime().toSecsSinceEpoch());
+      if (wpt->GetCreationTime().isValid()) {
+        buff = QString::asprintf(fmp.printfc.constData(), wpt->GetCreationTime().toSecsSinceEpoch());
+      }
       break;
     case XcsvStyle::XT_TIMET_TIME_MS:
       /* time as a time_t variable in milliseconds */
-      buff = QString::asprintf(fmp.printfc.constData(), wpt->GetCreationTime().toMSecsSinceEpoch());
+      if (wpt->GetCreationTime().isValid()) {
+        buff = QString::asprintf(fmp.printfc.constData(), wpt->GetCreationTime().toMSecsSinceEpoch());
+      }
       break;
     case XcsvStyle::XT_YYYYMMDD_TIME:
-      buff = QString::asprintf(fmp.printfc.constData(), time_to_yyyymmdd(wpt->GetCreationTime()));
+      if (wpt->GetCreationTime().isValid()) {
+        buff = QString::asprintf(fmp.printfc.constData(), time_to_yyyymmdd(wpt->GetCreationTime()));
+      }
       break;
     case XcsvStyle::XT_GMT_TIME:
       buff = writetime(fmp.printfc.constData(), wpt->GetCreationTime(), true);
@@ -1380,17 +1407,20 @@ XcsvFormat::xcsv_waypt_pr(const Waypoint* wpt)
     case XcsvStyle::XT_LOCAL_TIME:
       buff = writetime(fmp.printfc.constData(), wpt->GetCreationTime(), false);
       break;
-    case XcsvStyle::XT_HMSG_TIME:
-      buff = writehms(fmp.printfc.constData(), wpt->GetCreationTime(), true);
-      break;
-    case XcsvStyle::XT_HMSL_TIME:
-      buff = writehms(fmp.printfc.constData(), wpt->GetCreationTime(), false);
-      break;
     case XcsvStyle::XT_ISO_TIME:
-      buff = writetime("%Y-%m-%dT%H:%M:%SZ", wpt->GetCreationTime(), true);
+      if (wpt->GetCreationTime().isValid()) {
+        buff = wpt->GetCreationTime().toUTC().toString(Qt::ISODate);
+      }
       break;
     case XcsvStyle::XT_ISO_TIME_MS:
-      buff = wpt->GetCreationTime().toPrettyString();
+      if (wpt->GetCreationTime().isValid()) {
+        buff = wpt->GetCreationTime().toPrettyString();
+      }
+      break;
+    case XcsvStyle::XT_NET_TIME:
+      if (wpt->GetCreationTime().isValid()) {
+        buff = QString::number(qdatetime_to_dotnet_time(wpt->GetCreationTime()));
+      }
       break;
     case XcsvStyle::XT_GEOCACHE_LAST_FOUND:
       buff = QString::asprintf(fmp.printfc.constData(), time_to_yyyymmdd(wpt->gc_data->last_found));
@@ -1883,6 +1913,8 @@ XcsvFormat::rd_init(const QString& fname)
   if (xcsv_file->gps_datum_idx < 0) {
     fatal(MYNAME ": datum \"%s\" is not supported.", qPrintable(datum_name));
   }
+
+  utc_offset = (opt_utc == nullptr)? 0 : xstrtoi(opt_utc, nullptr, 10) * SECONDS_PER_HOUR;
 }
 
 void
diff --git a/xcsv.h b/xcsv.h
index 97d581f798088b70a6750db407cec53e109dc042..0cbfd13454b544a193c8b727f18b19fa5f368c85 100644 (file)
--- a/xcsv.h
+++ b/xcsv.h
 #include <utility>                // for move
 
 #include <QByteArray>             // for QByteArray
+#include <QDate>                  // for QDate
 #include <QDateTime>              // for QDateTime
 #include <QHash>                  // for QHash
 #include <QList>                  // for QList
 #include <QString>                // for QString
 #include <QStringList>            // for QStringList
+#include <QTime>                  // for QTime
 #include <QVector>                // for QVector
+#include <QtGlobal>               // for qRound64
 
 #include "defs.h"
 #include "format.h"
@@ -86,8 +89,6 @@ public:
     XT_GPS_SAT,
     XT_GPS_VDOP,
     XT_HEART_RATE,
-    XT_HMSG_TIME,
-    XT_HMSL_TIME,
     XT_ICON_DESCR,
     XT_IGNORE,
     XT_INDEX,
@@ -332,6 +333,11 @@ private:
     UrlLink* link_{nullptr};
     std::optional<bool> lat_dir_positive;
     std::optional<bool> lon_dir_positive;
+    QDate local_date;
+    QTime local_time;
+    QDate utc_date;
+    QTime utc_time;
+    bool need_datetime{true};
   };
 
   /* Constants */
@@ -346,20 +352,21 @@ private:
   }
 
   /* convert excel time (days since 1900) to time_t and back again */
-  static constexpr double excel_to_timet(double a)
+  static constexpr qint64 excel_to_timetms(double a)
   {
-    return (a - 25569.0) * 86400.0;
+    return qRound64((a - 25569.0) * 86400000.0);
   }
-  static constexpr double timet_to_excel(double a)
+  static constexpr double timetms_to_excel(qint64 a)
   {
-    return (a / 86400.0) + 25569.0;
+    return (a / 86400000.0) + 25569.0;
   }
 
   /* Member Functions */
 
-  static QDateTime yyyymmdd_to_time(const QString& s);
-  static time_t sscanftime(const char* s, const char* format, bool gmt);
-  static time_t addhms(const char* s, const char* format);
+  static QDate yyyymmdd_to_time(const QString& s);
+  QDateTime xcsv_adjust_time(const QDate date, const QTime time, bool is_localtime) const;
+  static void sscanftime(const char* s, const char* format, QDate& date, QTime& time);
+  static QTime addhms(const char* s, const char* format);
   static QString writetime(const char* format, time_t t, bool gmt);
   static QString writetime(const char* format, const gpsbabel::DateTime& t, bool gmt);
   static QString writehms(const char* format, time_t t, bool gmt);
@@ -391,6 +398,8 @@ private:
   char* prefer_shortnames = nullptr;
   char* xcsv_urlbase = nullptr;
   char* opt_datum = nullptr;
+  char* opt_utc = nullptr;
+  int utc_offset{};
 
   QString intstylefile;
 
@@ -428,6 +437,10 @@ private:
       "datum", &opt_datum, "GPS datum (def. WGS 84)",
       nullptr, ARGTYPE_STRING, ARG_NOMINMAX, nullptr
     },
+    {
+      "utc",   &opt_utc,   "Write timestamps with offset x to UTC time",
+      nullptr, ARGTYPE_INT, "-14", "+14", nullptr
+    },
   };
 
 };
index f83e6f01b9a625f154cfe7e38b110a806e1a3508..8c3efbcb48784c0e07a20a2d2de02f0b9a1f3444 100644 (file)
@@ -829,7 +829,7 @@ longitude)
       </section>
       <section xml:id="style_def_yyyymmdd">
          <title>YYYYMMDD_TIME</title>
-         <para>YYYYMMDD_TIME is the waypoint's creation time, if any.  It's a single
+         <para>YYYYMMDD_TIME is the waypoint's creation time in UTC, if any.  It's a single
    decimal field containing four digits of year, two digits of month,
    and two digits of date.   Internally it is a LONG INTEGER and thus
    requires a LONG INTEGER printf conversion.
@@ -843,54 +843,30 @@ longitude)
          <title>GMT_TIME</title>
          <para>GMT_TIME is the waypoint's creation time, in UTC time zone.  It uses the
    strptime conversion format tags.
+   It can be used to parse/print a date and time in one column,
+   or a date in one column and a time in another column.  If used to parse a date and a
+   time in separate columns the date and time can be in either order.
 </para>
-         <para>example:
-</para>
-         <screen>IFIELD GMT_TIME,"","%m/%d/%Y %I:%M:%D %p"
+<para>example with a a date followed by a time using a 12 hour clock and an AM/PM indication:</para>
+<screen>IFIELD GMT_TIME,"","%m/%d/%Y %I:%M:%S %p"
+</screen>
+<para>example with a a date followed by a time using a 24 hour clock:</para>
+<screen>IFIELD GMT_TIME,"","%Y/%m/%d"
+IFIELD GMT_TIME,"","%H:%M:%S"
 </screen>
-         <para>Search the web for 'strptime man page' for details strptime, but one
+<para>Search the web for 'strptime man page' for details strptime, but one
    such page can be found at
-
-
-
-
-
-
-
-            <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://www.die.net/doc/linux/man/man3/strptime.3.html">http://www.die.net/doc/linux/man/man3/strptime.3.html</link></para>
+   <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="http://www.die.net/doc/linux/man/man3/strptime.3.html">http://www.die.net/doc/linux/man/man3/strptime.3.html</link></para>
       </section>
       <section xml:id="style_def_localtime">
          <title>LOCAL_TIME</title>
          <para>LOCAL_TIME is the waypoint's creation time, in the local
- time zone.  It uses strptime conversion format tags.  See GMT_TIME for a
- reference.
-</para>
-         <para>example:
+   time zone.  It uses strptime conversion format tags.
+   It can be used to parse/print a date and time in one column,
+   or a date in one column and a time in another column.  If used to parse a date and a
+   time in separate columns the date and time can be in either order.
+   See GMT_TIME for examples and a reference.
 </para>
-         <screen>IFIELD LOCAL_TIME,"","%y-%m-%d"
-</screen>
-      </section>
-      <section xml:id="style_def_hmsgtime">
-         <title>HMSG_TIME</title>
-         <para>HMSG_TIME parses up to three time parts and am/pm string to add
-   this value to the previously parsed *_TIME field that contains
-   only a date.  On output, will print the time in UTC.
-</para>
-         <para>example:
-</para>
-         <screen>IFIELD HMSG_TIME,"","%02d:%02d:%02d %s"
-</screen>
-      </section>
-      <section xml:id="style_def_hmsltime">
-         <title>HMSL_TIME</title>
-         <para>HMSG_TIME parses up to three time parts and am/pm string to add
-   this value to the previously parsed *_TIME field that contains
-   only a date.  On output, will print the time in local time.
-</para>
-         <para>example:
-</para>
-         <screen>IFIELD HMSL_TIME,"","%dh%dm"
-</screen>
       </section>
       <section xml:id="style_def_isotime">
          <title>ISO_TIME</title>
index 2c4d062cea4a04bdb4ab4dd3c834809931f70992..36652438f1aafa4316000533914284a3989f73a9 100644 (file)
@@ -1,5 +1,6 @@
 <para>
-This option specifies the local time zone to use when writing times.  It
-is specified as an offset from Universal Coordinated Time (UTC) in hours.
-Valid values are from -23 to +23.
+This option specifies the local time zone to use when reading and writing times.  It
+specifies a Universal Coordinated Time (UTC) offset in hours.  For example, in the
+winter in Sweden the UTC offset is UTC+1, which corresponds to an option utc=1.
+Valid values are from -14 to +14.
 </para>